Next.js 13 Migration from Jumpit

Joy
jumpit
Published in
13 min readOct 31, 2023

안녕하세요, 점핏의 프론트엔드 개발자 Joy입니다. 😎
저희 점핏 플랫폼은 2021년 5월부터 CRA(Create-react-app) 기반으로 시작된 개발자 채용 플랫폼 입니다. 그러나 웹 트렌드의 새로운 파동과 저희 서비스의 지속적인 성장을 위하여, 최근 Next.js 프레임워크로 전환을 결정하였습니다. 이 글에서는 그 결정 과정, 그리고 Next.js로 넘어가며 진행한 첫 단계의 환경 설정에 관한 경험을 나누고자 합니다.

왜 Next.js를 선택했는가?

서버사이드 렌더링(SSR)의 장점

점핏은 현재 CRA를 통해 클라이언트 사이드 렌더링(CSR) 방식입니다. CSR은 브라우저에서 직접 콘텐츠를 렌더링하므로 초기 로딩 시간이 길어질 수 있습니다. Next.js는 SSR을 통해 초기 로딩 속도를 개선할 수 있는 강력한 해결책을 제공합니다.

SEO의 향상

Next.js의 SSR을 사용하면 검색 엔진이 웹페이지의 콘텐츠를 더 쉽게 인식하게 됩니다. 이는 저희 서비스의 검색 엔진 최적화(SEO)를 크게 강화할 수 있습니다.

소셜 미디어 공유의 품질 향상

CSR 기반의 웹페이지는 소셜 미디어에서의 미리보기에 문제가 있을 수 있습니다. 반면, SSR을 통한 초기 콘텐츠 제공은 이러한 문제를 해결해 줍니다.

뛰어난 개발 경험

Next.js는 다양한 기능을 제공하여 개발 프로세스를 효율화합니다:

  • 핫 모듈 교체(HMR): 개발 중 변경 사항을 실시간으로 확인할 수 있어, 작업 효율을 크게 향상시킵니다.
  • 자동 페이지 라우팅: 복잡한 라우팅 설정 없이도 쉽게 페이지를 추가하거나 수정할 수 있습니다.
  • API 통합 개발: Next.js의 /api 폴더를 사용하면 별도의 서버 설정 없이 API를 손쉽게 구현할 수 있습니다.

Next.js는 단순한 웹 프레임워크를 넘어서, 프론트엔드 개발의 효율성과 품질을 동시에 추구합니다. 다양한 기능과 풍부한 생태계 덕분에 저희는 Next.js를 선택하게 되었습니다. 이러한 선택은 단순한 기술적 결정을 넘어, 서비스의 미래와 사용자 경험을 위한 중요한 투자라고 생각하여 Next.js 를 선택하게되었습니.

그다음 저희처럼 Next.js로 Migration 하는곳에 조금이라도 도움을 드리고자 저희 Migration 준비과정에 대해서 공유 드리겠습니다.

Migration 과정의 전반적인 흐름

Migration 작업은 크게 몇 가지 단계를 거쳐 진행되었습니다.

  1. 환경 구축: Next.js 프로젝트 초기화 하고, 필요한 라이브러리와 플러그인을 설치하여 기반을 마련했습니다.
  2. 라우팅 구성: Next.js의 파일 기반 라우팅 시스템을 이용하여 앱라우팅으로 구성했습니다.
  3. 데이터 페치 최적화: 각 페이지에서 필요한 데이터를 SSR을 이용해 미리 가져오는 로직을 tanstack/react-query로 구성했습니다.
  4. 컴포넌트 재사용: 기존 CRA에서 사용하던 컴포넌트를 Next.js 환경에 맞춰 Server component와 Client component를 조절하며 작업하였습니다.
  5. 마이그레이션 작업 및 테스트
  6. 배포 및 모니터링

기초부터 탄탄하게: Next.js 환경 설정의 ABC

모든 건축물이 그렇듯이, 웹 프로젝트의 성패도 그 기반이 얼마나 견고하게 되어있는지에 따라 크게 좌우됩니다. 그 기반, 즉 환경 설정은 서비스의 전체적인 흐름과 성능을 결정짓는 핵심 요소라고 생각합니다. 이러한 이유로 저희 프론트파트에선 다양한 자료와 문서를 참고하며 각 설정의 의미(ex.네이밍, query-key 관리 등등)와 필요성을 깊게 고민하며 첫 단계의 환경 설정을 진행하게 되었습니다.

  1. 기존 Git Repository를 복사하여 새로운 Repository 만들기
  • mirror 옵션을 이용한 clone
// --mirror 옵션을 넣으면 커밋 이력까지 함께 갖고온다.
$ git clone --mirror { git repository 주소 }
  • 새로운 repository와 연결
// clone을 정상적으로 완료됐다면 repository명.git 파일이 생성되어 있을 것이다.
$ cd {프로젝트명}.git

// 위 명령으로 경로 이동 후 .git으로 변경한 디렉토리에서 아래 명령을 실행
$ git remote set-url origin { 새로운 repository 주소 }
  • 새로운 repository와 push
// --mirror 옵션으로 통째로 push 한다.
$ git push --mirror

2. create-next-app로 초기 설정하기

  • npx create-next-app@latest로 설치
필요한 dependencies 설정

3. 기존 프로젝트에 사용하던 기술스택을 Next.js에 통합하기 (사용하고있는 기술스택 중 Styled-Components와 React Query에 대해 중점적으로 언급하겠습니다)

  • typescript 적용으로 기존 tsconfig.json 설정과 통합하였습니다.
  • 필요한 라이브러리 설치 : 기존에 활용하던 라이브러리를 Next.js 프로젝트 환경에 새로 설치하였습니다. install @tanstack/react-query, react-redux, redux, axios, react-hook-form, styled-component, react-datepicker
  • 환경변수 설정 : 기존에 설정되어있던 react의 환경변수 prefix인 ‘REACT_APP’ 대신 브라우저단에서 액세스할 환경변수는 NEXT_PUBLIC_ 접두사를 붙였고, Node.js 환경에서만 사용할 환경변수는 REACT_APP 접두사를 제거하는 작업을 하였습니다.

4. Styled-components 설정하기

  • Next.js 에서 styled-components 스타일 로드되기전까지 페이지에 스타일이 없는 화면을 보게됩니다. 그리고 서버에서 렌더링된 HTML과 클라이언트에서 주입된 스타일 사이에 일시적인 불일치현상이 발생할 수 있습니다. 이런 현상을 FOUC(Flash of Unstyled Content) 문제라고 말하며 이를 해결하기 위해 어떻게 설정했는지 공식문서를 토대로 공유하겠습니다.
  • 먼저 styled-components API를 사용하여 렌더링 중에 생성된 모든 CSS 스타일 규칙과 해당 규칙을 반환하는함수를 수집하는 전역 레지스트리 구성 요소를 만들고 레지스트리에 수집된 스타일을 루트 레이아웃의 < head >태그에 삽입하는 useServerInsertedHTML hook을 사용하여 클라이언트 컴포넌트를 생성합니다.
'use client';

import React, { useState } from 'react';
import { useServerInsertedHTML } from 'next/navigation';
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';

export default function StyledComponentsRegistry({
children,
}: {
children: React.ReactNode;
}) {
const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());

useServerInsertedHTML(() => {
const styles = styledComponentsStyleSheet.getStyleElement();
styledComponentsStyleSheet.instance.clearTag();
return <>{styles}</>;
});

if (typeof window !== 'undefined') return <>{children}</>;

return (
<StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
{children as React.ReactChild}
</StyleSheetManager>
);
}

그다음 root layout의 자식들을 style registry component로 래핑합니다.

import StyledComponentsRegistry from './lib/registry'

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<StyledComponentsRegistry>{children}</StyledComponentsRegistry>
</body>
</html>
)
}

이렇게 함으로써 FOUC 문제에 대처할 수 있게 되었습니다.

5. React Query SSR 설정하기

  • Next.js 13 버전에 맞춰서 서버 컴포넌트에서 클라이언트 컴포넌트로 데이터를 전달하는 방식을 똑똑하게 사용하기 위해 서버에서 쿼리를 미리 가져온 다음 해당 쿼리를 queryClient로 dehydrate를 하고 dehydrate한 데이터를 hydrate state에 담아주고 클라이언트에서는 캐시되어있는 쿼리를 가져오게 하였습니다.
  • hydrate 방식으로 pre-fetch 해올 경우 장점은 props drilling 없이 가져온 쿼리를 컴포넌트 트리 아래의 모든 컴포넌트에서 사용할수 있는 장점이 있습니다.
  • Provider 는 다음과 같이 작성합니다
"use client";

import { QueryClientProvider } from "@tanstack/react-query";
import getQueryClient from "./getQueryClient";

const queryClient = getQueryClient();

export default function TanstackQueryWrapper({ children }) {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
  • 싱글톤 인스턴스 QueryClient를 만듭니다. 이렇게 하면 다른 사용자와 요청 간에 데이터가 공유되지않고 요청당 한번만 QueryClient를 생성할 수 있습니다.
import { QueryClient } from "@tanstack/react-query";
import { cache } from "react";

const getQueryClient = cache(() => new QueryClient());
export default getQueryClient;
  • Hydrate 요소를 클라이언트 컴포넌트로 래핑해서 클라이언트 컴포넌트에서 사용할 수 있도록 만듭니다. (실제 API 요청부분을 보여드릴 수 없어서 api 테스트로 예시를 보여드립니다🤣)
import { dehydrate, Hydrate } from "@tanstack/react-query";
import getQueryClient from "../getQueryClient";
import View from "./view";

export async function getData() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
const data = await res.json();
return data;
}

export default async function HydratedPosts() {
const queryClient = getQueryClient();
await queryClient.prefetchQuery(["posts"], getData);
const dehydratedState = dehydrate(queryClient);

return (
<Hydrate state={dehydratedState}>
<View />
</Hydrate>
);
}
  • 클라이언트 컴포넌트에서는 캐시된 데이터를 가져옵니다.
"use client";
import { useQueryClient } from "@tanstack/react-query";

export default function View() {
const queryClient = useQueryClient();
const data = queryClient.getQueryData(["posts"]);

return (
<div>
<h1>Posts</h1>
<ul>
{data?.map((posts) => (
<li key={posts.id}>{posts.title}</li>
))}
</ul>
</div>
);

부가적으로, React Query에서 효율적으로 Query Key 구조화를 시키는 방법에 대해서 공유합니다. 저희처럼 Query Key가 잘 관리되지 않는 분들에게 도움이 되었음 좋겠습니다.

  • 먼저, Query Key Factory를 설치합니다.
$ npm i @lukemorales/query-key-factory
  • 다음으로 Query Key를 정의합니다.
import { createQueryKeyStore } from '@lukemorales/query-key-factory'

export const queryKeys = createQueryKeyStore({
users: null,
todos: {
detail: (todoId: string) => [todoId],
list: (filters: TodoFilters) => ({
queryKey: [{ filters }],
queryFn: (ctx) => api.getTodos({ filters, page: ctx.pageParam }),
}),
},
})

지금까지, 점핏(Jumpit)에서 Next.js로의 전환 과정에서 겪은 환경 구축 및 styled-components 최적화 설정과 React Query에 대한 경험을 공유하였습니다. 프레임워크를 도입한다는건 결코 쉬운일이 아니였지만 점핏의 프론트엔드 파트는 이러한 기술적 변화와 최적화를 통해 사용자에게 더 나은 경험을 제공하기 위해 끊임없이 노력하고 있습니다. 이 과정을 통해 저희가 어떤 절차를 거치며, 어떠한 문제를 해결했는지를 알 수 있었기를 바랍니다.

이 글은 환경 설정에 대한 첫 번째 부분이며, 다음번 포스팅에서는 추가적으로 마이그레이션한 내용을 공유될 예정입니다.

점핏은 항상 더 좋은 서비스 제공을 위해 노력하겠습니다. 감사합니다. 🙇‍♀️

다음 포스팅에서 또 만나요!😁

--

--