tl;dr

StudyFront(구 Hypen) 프론트엔드 스택을 react +typescript + vite에서 Next.js v14로 변경했습니다.
많관부~🙏


배경

현재 재직중인 회사에서 서비스 중인 StudyFront에서는 사용자들끼리 입시에 관한 고민상담, 정보 공유를 위한 커뮤니티 서비스를 최근 새로 오픈했습니다.
커뮤니티 서비스를 오픈한 이유는 여러가지가 있었지만, 그 중 큰 비중을 차지한 한가지는 ‘신규 사용자의 유입’이었습니다.
이를 위해서는 **각 게시글에 대한 SEO(Search Engine Optimization)**가 이루어지는 것이 첫번째 관문이라고 생각되었습니다.

 

문제

초기 StudyFront(구 Hypen)는 개발단에서 빠른 속도와 테스트, 업데이트가 필요했고, 이를 위해 개발자의 숙련도(개발 속도), 초기 개발 환경 구성의 용이함에 초점을 맞춰 react + typescript + vite를 활용한 프로젝트로 구성되어 있었습니다.
해당 스택의 문제점은 커뮤니티 서비스가 새로 오픈하고 각 게시글 페이지 별로 메타태그를 적용해야하는 상황에서 부각되었습니다.
react는 기본적으로 **CSR(Client Side Rendering)**을 지원하는 SPA(Single Page Application) 라이브러리이고, CSR을 지원하는 SPA 라이브러리에서는 단 하나의 HTML 파일만을 사용하여 웹페이지를 렌더링하기 때문에 각 페이지별 SEO를 위한 메타태그를 적용하기 어려운 상황이었습니다.

📌 SPA(Single Page Application) : 기존의 웹페이지는 각 페이지마다 서버에서 HTML을 생성하여 클라이언트로 전달해주는 MPA(Multiple Page Application) 방식 이었습니다.

해당 방식은 UX측면, 서버의 부하등의 단점이 존재했는데, 이를 해결하기 위해 프로젝트 내에 단 하나의 HTML파일을 두고 페이지 이동 시, 해당 HTML 파일 내에서 Javascript를 활용하여 동적으로 페이지를 렌더링 하는 SPA 방식이 등장하게 되었습니다.

흔히 프론트엔드 프레임워크 3대장(react, vue, angular)이라고 불리는 프레임워크들이 해당 방식을 채택하고 있습니다.

 

해결 방안

해당 문제를 해결하기 위해 3가지의 선택지를 고려했습니다.

  1. react-snap + react-helmet을 활용한 정적 페이지 + 동적 메타태그 적용
  2. server-side-rendering을 위한 자체 서버 구축
  3. react => Next.js로의 마이그레이션
📌 Next.js는 react를 위한 프레임워크입니다. react에서 제공하지 않는 기능, 혹은 사용하기 복잡한 기능의 사용을 용이하게 도와줍니다. (ex. server-side-rendering, etc…)

 

1. react-snap + react-helmet을 활용한 정적 페이지 + 동적 메타태그 적용

react-snap 라이브러리는 업데이트 된지 5-6년 되었고, 동적 라우팅에 대해 제대로 대응하지 못한다는 문제점과 react-helmet 라이브러리는 동적으로 생성된 메타태그가 제대로 구글 크롤러봇에 의해 수집되지 못한다는 이슈가 있었기에 선택에서 제외했습니다.

📌 react-snap : 프로젝트 빌드시에 모든 경로의 페이지를 정적 HTML 파일로 빌드해주는 라이브러리입니다.
react-helmet : 동적으로 HTML head 태그 내에 메타태그를 삽입할 수 있게 해주는 라이브러리입니다.

 

2. server-side-rendering을 위한 자체 서버 구축

작성하신 글을 읽을 때마다 감탄하게 되는 Evan Moon님의 Vue Server Side Rendering 글을 보고 자체적으로 server side rendering 서버를 구축하는 것이 오히려 개발 기간을 단축시킬 수 있지 않을까 고민했습니다.
 
하지만 서버 구축 이후의 배포 과정, 적용 후 발생할 side effect와 이후의 확장성을 고려했을 때 해당 목적만을 지닌 서버를 구축하기 보다는 범용적으로 사용되고 있는 프레임워크의 힘을 빌리는 것이 조금 더 서비스를 위한 방법이라고 생각되었습니다.
 

3. react => Next.js로의 마이그레이션

최종적으로 Next.js로 마이그레이션 하는 작업이 가장 안정적이고 빠르게 문제를 해결할 수 있다고 생각되었습니다.


Next.js를 알기 전에 알아야 할 지식

마이그레이션 과정을 설명하기 전에 Next.js를 사용하기 위해 알고 있으면 좋을 지식들을 먼저 간략하게 정리합니다.

 📌 렌더링(Rendering) : 웹 프론트엔드 개발에서 렌더링은 코드가 렌더링 엔진(Blink, Gecko, Webkit)에 의해 브라우저에 그려지는 과정을 이야기기합니다.
크게 DOM 트리, CSSOM 트리 생성 → 렌더링 트리 생성 → 레이아웃 → 페인팅의 단계로 이루어집니다.

 

CSR(Client Side Rendering)

CSR은 말 그대로 클라이언트 측에서 모든 렌더링을 진행하는 것입니다.
서버측에서는 초기에 SPA의 기본 뼈대가 되는 skeleton HTML(index.HTML)을 전달해 준 뒤에 Javascript 파일을 전송합니다.
클라이언트는 초기 HTML의 DOM트리를 생성한 후에 Javascript 코드 요청을 보내고 받아온 Javascript 코드의 내용에 따라 기존 DOM트리를 확장하여 동적으로 DOM트리를 생성, 조작하게 됩니다.
react는 이 과정에서의 DOM트리 조작 최적화를 위해 Virtual DOM을 활용합니다.
 

SSR(Server Side Rendering)

SSR은 서버 측에서 먼저 페이지에 보여질 HTML을 생성한 후에 클라이언트에게 완성된 HTML을 전달합니다.
클라이언트는 완성된 HTML을 통해 DOM트리를 생성하게 되고, 사용자와의 상호작용(ex. click, focus, etc…)을 위해 Javascript 코드를 받아와 hydrate의 과정을 통해 HTML에 Javascript 로직을 주입하는 거치게 됩니다.
 

Server Component

React Server Component(RSC)는 Meta의 react 팀에서 새로 도입한 기술입니다.
Dan Abramov(전 Meta react 팀, redux 창시자)에 의해 2020년에 처음으로 발표 후 react v18에서의 실험적 기능을 거쳐 Next.js v13의 app router 방식에 사용 되었습니다. (개인적으로 좋아하는 Dan의 글도 첨부합니다.)
 

Component?

📌 컴포넌트(component) : 리액트에서 사용되는 독립된 블럭 단위의 재사용 가능한 ui 코드를 뜻합니다.

 
기존 리액트의 컴포넌트는 모두 클라이언트에서 생성되는 컴포넌트였습니다. 서버와의 통신은 데이터를 갱신할 때만 이루어 졌고, 갱신된 데이터에 따라 컴포넌트는 동적으로 ui를 생성했습니다.
 
즉 기존 컴포넌트는 서버의 자원에 직접적으로 접근할 수 없었고, 서버에서 허용된 정보만을 요청을 통해 받아올 수 있었습니다.
 

react는 서버 컴포넌트를 통해 기존 컴포넌트가 갖고 있던 불편함을 해소할 수 있는 방법을 제시했습니다.
 
서버 컴포넌트는 서버에서 생성되는 컴포넌트입니다.
위의 SSR과 조금 헷갈릴 수 있지만 가장 큰 차이는 SSR은 string HTML을 반환하게 되고, 서버 컴포넌트는 RSC(React Server Component)를 payload 형태로 반환한다는 점입니다.
 

 

Server Component의 특징

  • 서버에서 생성되기 때문에 서버 자원(파일 시스템, DB, etc…)에 직접적으로 접근할 수 있습니다.
  • 이미 서버에서 생성된 컴포넌트를 받기 때문에 컴포넌트를 구성하기 위한 추가적인 Javascript 코드의 양을 줄일 수 있게 됩니다.
  • 서버에서 랜더링 되기 때문에 랜더링에 필요한 데이터를 가져오는 시간과 클라이언트가 요청하는 횟수를 줄여서 성능 향상에 도움을 줄 수 있습니다.
  • 서버에서 생성되고 관리되기 때문에 기존 component의 life cycle을 관리하는 hook( useState, useEffect)과 커스텀 hook을 사용하지 못합니다.
  • 마찬가지로 서버에서 생성되기 때문에 브라우저에서 접근 가능한 window api에 접근하지 못합니다.
  • DOM을 통해 사용자와 상호작용할 수 있는 Event Listener를 사용하지 못합니다.
  • 현재 React Server Component는 함수형 component만을 지원하기에 class형 component는 사용할 수 없습니다.

Next.js v13

📌 Next의 최신 버전은 14이고 현재 StudyFront에서도 14버전을 사용하고 있지만 대대적인 변화가 생긴 버전은 13이기에 13버전의 변화를 중점으로 정리하겠습니다.

 
Next.js는 react를 위한 프레임워크입니다. 이전 Next.js v12에 대한 내용은 여기에 정리해 놓았습니다.
react에서 server component를 공개한 후 Next에서도 server component를 활용한 새로운 아키텍처와 기능을 추가하였습니다.
 

app router

기존 Next.js는 pages router 방식을 사용하여 page 폴더 내에 배치한 폴더 구조에 따라 url routing을 지원했습니다.
이후 server component를 좀 더 용이하게 사용하고자 Next.js 에서는 새로 app router방식을 공개했습니다. (pages router 방식을 사용할 수도 있습니다.)
 
app router를 사용하게 된다면 프로젝트 내 컴포넌트는 기본적으로 서버 컴포넌트를 사용하게 됩니다.
app router 방식에서도 app 디렉토리 내의 폴더 구조를 통해 라우팅이 되고 각 폴더의 page파일을 해당 경로의 기본 페이지로 인식합니다. (각 폴더 명이 경로명으로 적용됩니다.)
 

StudyFront 중첩 구조

 
위의 예시를 보면 폴더명에 따라 url은 www.studyfront.co.kr/calculator 가 되고 해당 주소로 진입했을 때 가장 먼저 보여지는 ui는 page.tsx가 됩니다. (app 디렉토리 내 root 경로의 page.tsx파일은 www.studyfront.co.kr/ 의 초기 ui를 보여주게 됩니다.)
 
페이지별 중첩 구조
 

출처 :  https://nextjs.org/docs/app/building-your-application/routing

 
app router 내 각 폴더(url 경로)는 위의 구조를 갖게 됩니다.

  • layout : 페이지를 이루는 컴포넌트 중 가장 상위의 개념이 됩니다. 페이지에서 공통으로 적용하는 ui를 담을 수 있습니다. 하위 컴포넌트의 상태가 변경되어도 재렌더링 되지 않습니다.
  • page : 경로에 따른 페이지 고유의 ui를 담을 수 있는 컴포넌트입니다. 페이지에서 보여주어야 하는 핵심 ui를 보여줍니다.
  • template : layout과 비슷하게 공통으로 적용하는 ui를 담을 수 있지만, 하위 페이지의 상태 변경시 재렌더링이 되어야 하는 ui를 담아야 합니다.
  • error : 페이지 내 에러 발생시 보여지는 ui를 담고 있습니다.
  • loading : 페이지 내 로딩 발생시 보여지는 ui를 담고 있습니다.
  • not-found : 해당 경로에 존재하는 페이지가 없을 경우 보여지는 ui를 담고 있습니다.
  • global-error : app 디렉토리의 root 경로에만 추가할 수 있는 파일로 전역 에러를 담당하는 ui를 담고 있습니다.
📌 app router에서는 react의 SuspenseError boundary를 적극적으로 활용하고 있기 때문에 각 페이지 별로 loading.tsx, error.tsx를 통해 선언적으로 로딩 상태와 에러 상태를 관리해줄 수 있습니다.

 

HTML Streaming

서버에서 렌더링 되는 페이지의 경우 사용자가 페이지를 이용하기까지 몇가지 필요한 과정들을 거쳐야 합니다.

  1. 페이지에서 필요한 데이터 가져오기
  2. 서버에서 페이지의 HTML 렌더링하기
  3. 페이지의 HTML, CSS, Javascript를 클라이언트로 전달하기
  4. 아직 사용자와의 상호작용이 되지 않는 페이지를 HTML, CSS로 보여주기
  5. hydrate 과정을 통해 사용자와의 상호작용이 되도록 적용하기
출처 :  https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming

 
위의 단계들은 순차적이고 다음 단계의 진행을 차단합니다. 또한 react는 Javascript hydration이 일어난 이후에 ui를 표현해 줄 수 있기 때문에, 사용자는 위의 모든 단계가 끝나기 전까지는 빈 페이지를 보게 됩니다.
 

출처 :  https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming

 
HTML streaming은 서버에서 렌더링 되는 HTML을 좀 더 작은 단위의 chunk로 나누어서 점진적으로 받아올 수 있게 합니다.
이를 통해 사용자는 페이지의 먼저 렌더링 된 부분부터 순차적으로 볼 수 있게 됩니다.
 
 
Next.js v12 이상부터 서버에서 렌더링되는 페이지는 자동으로 HTML streaming이 적용됩니다.
HTML streaming과 react Suspense를 활용하여 좀 더 세밀하게 ux를 향상시킬 수도 있습니다.

import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'

export default function Posts() {
  return (
    <section>
      <Suspense fallback={<p>Loading feed...</p>}>
        <PostFeed />
      </Suspense>
      <Suspense fallback={<p>Loading weather...</p>}>
        <Weather />
      </Suspense>
    </section>
  )
}

 


마이그레이션 적용기

📌 Next.js는 react에서 각 스펙에 따른 마이그레이션 방법을 문서로 제공합니다. StudyFront는 빌드 툴로 vite를 사용하고 있었기에 기본적인 설정은 해당 문서를 참고하여 마이그레이션을 진행하였습니다.

 

recoil 사용하기

Next.js v13 이후 기본 컴포넌트가 서버 컴포넌트로 적용되기 때문에, 프론트엔드 전역 상태관리를 위한 라이브러리인 recoil을 사용하려면 추가적인 작업을 해주어야 합니다.
recoil을 전역으로 사용해주기 위해 모든 컴포넌트의 상위에 <RecoilRoot> 컴포넌트를 선언해 주어야 하는데 RecoilRoot컴포넌트는 내부적으로 react의 context api를 사용하고 있습니다.
context api는 프론트엔드 측에서 상태관리를 위해서 사용하는 react의 스펙으로, 구독하는 값이 변경될 때마다 구독을 신청한 컴포넌트를 재렌더링 합니다.
이때 서버측에서 렌더링이 진행되는 서버 컴포넌트의 경우 구독하는 값이 변경됨에 따라 컴포넌트를 계속해서 새로 그리게 된다면 서버에 부하를 일으킬 수 있습니다.
또한 react에서 서버 컴포넌트를 만든 이유는 서버측의 데이터를 반영하는데에 중점을 가진 컴포넌트를 만들고자 함이었는기 때문에 프론트엔드 측의 사용자와의 상호작용을 위한 값과는 별도로 관리를 위해 서버 컴포넌트에서는 context api를 사용할 수 없도록 만들었습니다.
그런 이유로 Next.js에서 recoil을 사용하기 위해서는 클라이언트 컴포넌트로 <RecoilRoot>를 감싸주는 과정을 진행해야 합니다.

// Next.js에서 클라이언트 컴포넌트를 사용하기 위해서는 컴포넌트 파일 상단에 해당 문구를 추가해야합니다.
'use client';

import React, { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';

const RecoilWrapper = ({ children }: { children: ReactNode }) => {
  return <RecoilRoot>{children}</RecoilRoot>;
};

export default RecoilWrapper;

 
이후 app 디렉토리 최상단의 layout.tsx에서 <RecoilWrapper> 컴포넌트로 children 속성을 감싸줍니다.

import React, { ReactNode } from 'react';
import RecoilWrapper from '../RecoilWrapper';

const RootLayout = ({ children }: { children: ReactNode }) => {
  return (
    <html lang="en">
      <body>
        <div id="root">
          <RecoilWrapper>
              <Template>{children}</Template>
          </RecoilWrapper>
        </div>
      </body>
    </html>
  );
};

export default RootLayout;

 
 

styled components 사용하기

Next.js 공식문서에서는 styled components를 사용하기 위한 설정을 문서로 제공하고 있습니다.
해당 문서에서는 SSR을 위한 설정까지는 제공하고 있지만 문서 최상단에 해당 경고가 있습니다.
 

Warning: CSS-in-JS libraries which require runtime JavaScript are not currently supported in Server Components. Using CSS-in-JS with newer React features like Server Components and Streaming requires library authors to support the latest version of React, including concurrent rendering.
We're working with the React team on upstream APIs to handle CSS and JavaScript assets with support for React Server Components and streaming architecture.

경고: 런타임 자바스크립트가 필요한 CSS-in-JS 라이브러리는 현재 서버 컴포넌트에서 지원되지 않습니다. 서버 컴포넌트 및 스트리밍과 같은 최신 React 기능과 함께 CSS-in-JS를 사용하려면 라이브러리 작성자가 동시 렌더링을 포함한 최신 버전의 React를 지원해야 합니다. 저희는 React 서버 컴포넌트 및 스트리밍 아키텍처를 지원하는 CSS 및 JavaScript 에셋을 처리하기 위해 업스트림 API에 대해 React 팀과 협력하고 있습니다.

 
현재 Next.js에서 Server Component를 사용시 CSS-in-JS 라이브러리는 지원하지 않고 있음을 알 수 있습니다.
해당 이슈를 해결하기 위해서 StudyFront에서는 서버 컴포넌트가 필요한 컴포넌트내의 스타일을 별도의 스타일 파일을 생성 후 기본 css를 적용하여 해결하였습니다.
 

 

error handling

Next.js에서는 react의 Error boundary를 활용하여 선언적 방식으로 에러를 처리할 수 있게 합니다.
경로 폴더 내 error 파일을 추가하여 ui 렌더링 시 에러가 발생하면 error ui를 노출하게 됩니다.
만일 해당 경로 폴더에 error 파일이 없다면, 상위 폴더의 error ui를 노출하게 됩니다.
Server Component의 경우 컴포넌트 내에서 발생한 에러는 가장 가까운 error 파일로 전달되게 됩니다.
Error boundary는 렌더링 과정에서의 오류만을 포착하기에 해당 스펙을 사용할 경우 포착할 수 없는 오류들이 몇가지 존재합니다.

  • 이벤트 핸들러 내의 오류
  • 비동기 코드 오류 (Promise, setTimeout, setInterval, async/await, etc…)
  • Error boundary 자체의 오류
  • 렌더링 이외의 모든 오류…

이러한 부분들 중 비동기 관련 코드는 서버에서 데이터를 받아올 때 필수적으로 사용되기 때문에 StudyFront 프로젝트에서는 컴포넌트 내 state를 사용하여 오류를 error 파일로 전달하고 있습니다.

'use client';

import React, { ReactNode, useEffect, useState } from 'react';
import { useRouter } from 'Next/navigation';
import { useSetRecoilState } from 'recoil';

import { fetchGetUserInfo } from '../../core/fetcher/user';
import { getStorage } from '../../core/util/function';
import { userState } from '../../store/user';

const PrivateTemplate = ({ children }: { children: ReactNode }) => {
  const router = useRouter();
  const setUserInfo = useSetRecoilState(userState);

  // 오류 상태
  const [, setError] = useState(null);

  useEffect(() => {
    if (getStorage('token') === null) {
      router.replace('/?link=login');
    } else {
      (async () => {
        try {
          const result = await fetchGetUserInfo();
          setUserInfo(result);
        } catch (error: any) {

        // setError를 통해 오류를 던지는 함수를 곧바로 실행
          setError(() => {
            throw error;
          });
        }
      })();
    }
  }, []);

  return <>{children}</>;
};

export default PrivateTemplate;

 
 

react-router-dom → next/navigation

react-router-dom은 react에서 라우팅을 적용하기 위한 라이브러리입니다.
react 프로젝트에서는 클라이언트 측 라우팅을 위해 모든 페이지를 연결하는 route를 선언하고 실행할 수 있게 합니다. 즉 해당 route를 통해 url 경로에 따라 서로 다른 컴포넌트를 노출하도록 설정할 수 있습니다.

// 예시 코드이기 때문에 별도의 import 코드는 추가하지 않았습니다.

const Router = () => {
  const isLogin = useRecoilValue(loginState);

  const router = createBrowserRouter([
    {
      // url path에 따라 다른 컴포넌트를 노출하도록 설정해 줍니다.
      path: '/',
      element: (
        <ErrorBoundary FallbackComponent={ErrorComponent}>
          <Template>
            <NewHome />
          </Template>
        </ErrorBoundary>
      ),
    },
    {
      path: '/mypage',
      element: isLogin ? (
        <ErrorBoundary FallbackComponent={ErrorComponent}>
          <Suspense fallback={<Loading />}>
            <Template>
              <MyPageTemplate />
            </Template>
          </Suspense>
        </ErrorBoundary>
      ) : (
        <Navigate to="/" />
      ),
      children: [
        {
          path: 'my',
          element: <MyInfo />,
        },
        {
          path: 'life_record',
          element: <LifeRecordHistory />,
        },
        {
          path: 'result',
          element: <MyPageResultTemplate />,
          children: [
            {
              path: 'early',
              element: <EarlyResultHistory />,
            },
            {
              path: 'regular',
              element: <RegularResultHistory />,
            },
          ],
        },
        {
          path: 'purchase',
          element: <PurchaseHistory />,
        },
      ],
    },
    {
      path: '/calculator',
      element: (
        <Suspense fallback={<Loading />}>
          <Template>
            <Calculator />
          </Template>
        </Suspense>
      ),
    },
    {
      path: '*',
      element: <Navigate to="/" />,
    },
  ]);

  return <RouterProvider router={router} />;
};

export default Router;

 
하지만 Next.js는 내부적으로 폴더 구조 기반의 라우팅을 제공하고 있기 때문에 해당 react-router-dom을 사용할 필요가 없습니다.

 

문제

react-router-dom 라이브러리에는 페이지간 이동(navigating & linking)에 필요한 여러가지 hook을 제공하고 있습니다.
react-router-dom의 대표적인 hook

  • useNavigate : 현재 페이지에서 다른 페이지로 이동하는 기능을 제공합니다. (window.history api를 사용하여 경로간 정보(state)를 전달할 수도 있습니다.)
  • useLocation : 현재 페이지에서 가지고 있는 정보(현재 경로의 이름, 이전 경로에서 전달해준 정보(state), etc…)들에 접근할 수 있는 기능을 제공합니다.
  • useParams : 동적 라우팅에 사용되는 path parameter 값을 가져올 수 있는 기능을 제공합니다.
  • useSearchParams : query parameter 값을 key / value형식으로 가져올 수 있는 기능을 제공합니다.

react-router-dom을 사용하지 않게 되면서 Next에서는 navigating과 linking을 구현하기 위해 next/navigation ****패키지를 사용하게 되었습니다.
이때 react-router-dom에서 제공하는 hook도 사용을 못하기에 프로젝트 전반에 있는 navigating & linking에 관련된 모든 hook들을 next/navigation에서 제공하는 hook으로 교체하는 작업을 진행했습니다.

📌 https://github.com/vercel/next.js/discussions/48426
현재 Next.js에는 navigating & linking을 적용하기 위한 패키지가 두가지 존재합니다.

next/router : Next.js 13버전 이전에 사용되던 패키지로 가볍고 적은 기능을 가지고 있습니다. app router 방식에는 사용할 수 없습니다.

next/navigation : Next.js 13버전 이후에 개발된 패키지로 next/router와 비교하여 더 많은 기능을 가지고 있고 modern Next.js 프로젝트에 권장됩니다.

 
 
next/navigation의 대표적인 hook

  • useRouter : 페이지 이동, 페이지 새로고침 등 경로와 관련된 기능을 제공합니다.
  • useParams : 동적 라우팅에 사용되는 path parameter 값을 가져올 수 있는 기능을 제공합니다.
  • userSearchParams : get 메서드를 통해 query parameter 값의 key를 인자로 받아 value 값을 가져오는 기능을 제공합니다.
  • usePathname : 현재 페이지의 경로를 가져오는 기능을 제공합니다.

하지만 교체하는 과정에서 문제가 생겼습니다.
 
기존 useNavigate hook은 state 매개변수를 통해 현재 경로에서 이동하려는 경로로 데이터를 전달할 수 있었습니다. 하지만 next/navigation의 useRouter hook은 해당 기능을 제공하지 않고 있었습니다. (개인적인 추측으로는 SSR 방식과 CSR 방식의 컴포넌트를 혼합하여 사용할 수 있는 Next.js의 특성클라이언트 측의 보안과 성능을 이유로 해당 기능을 제공하지 않는 것으로 생각됩니다.)
 
그런 이유로 기존에 페이지를 이동하며 전달해 주었던 정보를 다른 방식으로 이동한 페이지에서 접근할 수 있도록 수정해 주어야 했습니다.

 

해결

StudyFront에서는 해당 문제를 sessionStorage에 정보를 저장하는 방식과 url query parameter로 전달하는 방식을 데이터의 특성에 따라 적절히 사용하는 방식으로 해결하였습니다.

 

Metadata

Next.js로 마이그레이션을 진행한 가장 주된 이유입니다.
Next.js에서는 Metadata를 적용할 수 있도록 컨벤션을 제공하고 있습니다.
메타데이터는 서버에서 생성되는 컴포넌트에 적용할 수 있기 때문에, 적용 전에 Server Component와 Client Component를 섞어서 사용하는 기본적인 패턴에 대해 알아야 합니다.

  • Server Component 내부에서 Client Component를 import하여 사용하는 것은 가능합니다.
  • Client Component 내부에서 Server Component를 import하여 사용하는것은 불가능합니다. => 사용하기 위해서는 Server Component를 Client Component의 props로 전달하여 사용해야 합니다.

이러한 패턴을 토대로 각 페이지의 Layout.tsx을 Server Component로 선언함과 동시에 Metadata를 생성해 주었습니다.

// StudyFront 메인 페이지

import React, { ReactNode } from 'react';
import type { Metadata } from 'next';

import Template from '../components/_d_template/Template';
import RecoilWrapper from '../RecoilWrapper';
import GlobalStyle from '../style/GlobalStyle';
import StyledComponentsRegistry from '../style/registry';

export const metadata: Metadata = {
  title: '스터디프론트',
  description:
    '내가 갈 대학 예측을 AI와. 생기부 1인자가 되고 싶다면 스터디프론트에서',
  keywords: [
    '스터디프론트',
    'studyfront',
    '생기부',
    '생활기록부',
    '글자수 세기',
    '입시',
    '입시캐스트',
    '정시 예측',
    '수시 예측',
    '모의고사',
    '입시 커뮤니티',
    '입시 정보',
    '컨설팅',
    '의대',
  ],
  openGraph: {
    title: '입시의 최전선 | 스터디프론트 STUDYFRONT',
    description:
      '내가 갈 대학 예측을 AI와. 생기부 1인자가 되고 싶다면 스터디프론트에서',
    siteName: '입시의 최전선 | 스터디프론트 STUDYFRONT',
    url: '<https://www.studyfront.co.kr>',
    type: 'website',
    images: [
      {
        url: '<https://www.studyfront.co.kr/assets/opengraph_img.png>',
      },
    ],
  },
  alternates: {
    canonical: '<https://www.studyfront.co.kr/>',
  },
};

const RootLayout = ({ children }: { children: ReactNode }) => {
  return (
    <html lang="en">
      <body>
        <div id="root">
          <GlobalStyle />
          <RecoilWrapper>
            <StyledComponentsRegistry>
              <Template>{children}</Template>
            </StyledComponentsRegistry>
          </RecoilWrapper>
        </div>
      </body>
    </html>
  );
};

export default RootLayout;
 

 
페이지 경로를 동적으로 받아오는 경우 generateMetadata 메서드를 사용하여 동적으로 Metadata를 생성해 줄 수 있습니다.

// 커뮤니티의 입시의 모든 것 개별 게시글 페이지

import React, { ReactNode } from 'react';
import { Metadata } from 'next';

export const generateMetadata = async ({
  params,
}: {
  // url 경로의 path parameter를 통해 게시글의 id를 가져옵니다.
  params: { post_id: string };
}): Promise<Metadata> => {
  const postId = params.post_id;

  // path parameter를 통해 받아온 id를 사용하여 서버에서 해당 게시글의 정보를 불러옵니다.
  const result = await fetch(
    `${process.env.NEXT_PUBLIC_API_URL}/board/goal/${postId}`,
    {
      headers: {
        'Content-type': 'application/json',
      },
    },
  ).then((res) => res.json());

  const post = result.result[0];

  // 불러온 게시글을 바탕으로 메타데이터를 정의해 줍니다.
  return {
    title: ` ${post.title} | 스터디프론트 입시의 모든 것`,
    description: `${post.content}`,
    keywords: [
      '고등학교 추천',
      '학원 추천',
      '대학 입시 고민',
      '생기부 수정',
      '입시변화',
      '입결',
      '의대 공대 진로고민',
      '입시의 모든 것',
      `${post.title}`,
    ],
    openGraph: {
      title: `${post.title} | 스터디프론트 입시의 모든 것`,
      description: `${post.content}`,
      siteName: '입시의 모든 것 | 스터디프론트 커뮤니티',
      images: ['https://www.studyfront.co.kr/assets/opengraph_img.png'],
      url: `https://www.studyfront.co.kr/community/goal/detail/${postId}`,
      type: 'website',
    },
    alternates: {
      canonical: `https://www.studyfront.co.kr/community/goal/detail/${postId}`,
    },
  };
};

const GoalDetailLayout = ({ children }: { children: ReactNode }) => {
  return <>{children}</>;
};

export default GoalDetailLayout;

위의 과정들을 통해 다행히 큰 이슈 없이 StudyFront의 Next.js 마이그레이션을 완료했습니다.
 
Next.js는 업데이트 주기가 빠르고 새로운 기술들을 적극적으로 적용하는 프레임워크입니다.
현재 웹 개발의 여러 부분에서 가장 선두적인 행보를 보이는 Vercel이 개발을 주도하고 있기에, 앞으로도 웹 개발 시장을 한동안 주도할 것으로 생각됩니다.
 
react에서 Next.js로 변경하는 과정에서 성가신 부분도 있었고 막막한 부분도 있었지만, 적용하고 나니 프로젝트가 조금 정돈된 부분을 느끼면서 프레임워크의 장점을 조금 체감하게 되었습니다.
동시에 프레임워크의 편리한 부분들의 이면에 감춰진 적용된 기술들을 알기 위해 노력하는 것도 프레임워크를 잘 사용하기 위해서는 필요한 과정이라고 생각되었습니다.


출처

 
Next.js 14 변경점

Next.js 14 업데이트 살펴보기 | 요즘IT

지난 10월 26일, Next.js 14가 발표되었습니다. 13 버전 업데이트의 변화가 워낙 커서 그런지 이번에는 상대적으로 변경 사항이 적게 느껴지기도 했습니다. 이번 업데이트에서는 13 버전에서 소개된 A

yozm.wishket.com

Next.js 14

Next.js 14 includes included performance, stability for Server Actions, a new course teaching the App Router, and more.

nextjs.org

 
Server Component

React Server Components

JavaScript번들에 포함되지 않고 SSR을 보완하는 중간 추상화 렌더링 - zero-bundle-size React Server Components 서버 드리븐 멘탈 모델을 통한 모던 UX를 구현하는데 초점이 맞추져 있으며 현재 React…

patterns-dev-kr.github.io

React 18: 리액트 서버 컴포넌트 준비하기 | 카카오페이 기술 블로그

공식 릴리즈 전인 리액트 서버 컴포넌트에 대해 알아보고 준비해 봅니다.

tech.kakaopay.com

Rendering: Server Components | Next.js

Learn how you can use React Server Components to render parts of your application on the server.

nextjs.org

 
렌더링

웹 브라우저의 렌더링이란? | 요즘IT

우리가 인식하지 못할 뿐, 웹페이지는 미리 만들어진 것을 가져오는 게 아니라 실시간으로 그려지는 것에 가깝습니다. 실시간으로 웹사이트가 그려지는 과정, 이 과정을 웹 브라우저의 렌더링

yozm.wishket.com


 

+ Recent posts