ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • next.js 13 page directory를 app directory 로 마이그레이션하기
    Web/Next.JS 2023. 2. 22. 04:52

    next 13이 나오고 많은 기능이 새로 나왔다.

     

    1. app Directory 업데이트 소식,

    2. 터보팩

    3. 새로운 next/image

    4. 새로운 @next.font

    5. next/link <a> 없이 가능하게 변경

     

    그중 app Directory를 사용할 때 몇가지 차이점은

    1. layout : layout 구조를 사용하여 경로간 ui를 공유하고 재렌더링을 피할수있다.

    2. server components : react 18에서 지원하는 서버 컴포넌트를 기본값으로 만든다.

    3. streaming : 데이터를 필요로 하지 않는 페이지는 바로 렌더링하고 데이터를 불러오는 페이지는 로딩 상태로 표시 할 수 있는 기능

    4. Data Fetching :  react에서 fetch를 확장시켜 적용하였는데 cache 속성을 이용해  ssg, ssr, isr을 하나의 api로 사용할 수 있다.

     

     

     

    아직 베타버전인 app directory로 마이그레이션 하기로 마음먹은 이유는 2,4 번 때문인데 

    서버 컴포넌트를 잘 활용하면 javascript 번들 크기를 크게 줄일 수 있어 성능 향상이 기대되기 때문이고,

    ssg, ssr, isr을 fetch 하나로 해결 할 수 있다는 점이였다.

     

     

    마이그레이션 과정

    1단계 : next.config.js 코드 추가, app 디렉토리 생성

     

    먼저 next.config.js 파일에 app dir을 사용하겠다고 말해두어야 한다.

    /** @type {import('next').NextConfig} */
    const nextConfig = {
      experimental: {
        appDir: true,
      },
    };
    
    module.exports = nextConfig

     

    그 다음 src/app 이라는 이름의 새 폴더를 만든다.

     

     

    2단계 : root layout 생성

     

    app/layout.tsx 라는 파일을 생성한다. 이 root layout 은 모든 경로의 페이지에 적용이 된다.

     

    export default function RootLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      return (
        <html lang="en">
          <body>{children}</body>
        </html>
      );
    }

    위는 next 문서에 나온 예시인데, 현재 프로젝트 에서 react-query, styled-compoenets를 사용하고 있어 따로 몇가지 필요한 코드를 더 추가하였다. 

    import { theme } from "@chooz/ui";
    import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
    import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
    import type { AppProps } from "next/app";
    import styled, { ThemeProvider } from "styled-components";
    import { GlobalStyles } from "styles/globalStyles";
    import Header from "../components/header/Header";
    
    function RootLayout({ Component, pageProps }: AppProps) {
      const queryClient = new QueryClient();
      return (
        <QueryClientProvider client={queryClient}>
          <ReactQueryDevtools />
          <ThemeProvider theme={theme}>
            <GlobalStyles />
            <Applayout>
              <Header />
              <Component {...pageProps} />
            </Applayout>
          </ThemeProvider>
        </QueryClientProvider>
      );
    }
    
    const Applayout = styled.div`
    	//css
    `;
    
    export default App;

    기존에 page directory 에서 사용하던 _document.js, _app.js 를 옮겨주었다.

    추가적으로 _document.js 에서 사용하던 웹폰트들은 app/head.tsx에 옮겨주었다.

    // app/head.tsx
    
    export default function Head() {
      return (
        <>
          <title>My Page Title</title>
          <link
            as="style"
            href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/variable/pretendardvariable.css"
            rel="stylesheet"
            // @ts-ignore
            precedence="default"
          />
          <link
            as="style"
            href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendardstatic.css"
            rel="stylesheet"
            // @ts-ignore
            precedence="default"
          />
        </>
      );
    }

     

    (precedence 속성은 https://beta.nextjs.org/docs/api-reference/file-conventions/head#supported-head-tags 를 참고하여 넣게 되었다.)

     

    또한 styled-compoent 관련 ssr 설정은 

    src/lib/registry.tsx 에 따로 설정해 두어야 한다.

    "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 }) {
      // Only create stylesheet once with lazy initial state
      // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
      const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
    
      useServerInsertedHTML(() => {
        const styles = styledComponentsStyleSheet.getStyleElement();
        // @ts-ignore
        styledComponentsStyleSheet.instance.clearTag();
        return <>{styles}</>;
      });
    
      if (typeof window !== "undefined") return <>{children}</>;
    
      return (
        <StyleSheetManager sheet={styledComponentsStyleSheet.instance}>{children}</StyleSheetManager>
      );
    }

     

    next.config.js에 styled-component를 적용한다고 선언한다.

    // next.config.js
    
    /** @type {import('next').NextConfig} */
    const nextConfig = {
      compiler: {
        styledComponents: true,
      },
    };
    
    module.exports = nextConfig;

     

    이것까지 적용한 최종 코드들은 다음과 같다.

     

     

    layout.tsx

    "use client";
    
    import { theme } from "@chooz/ui";
    import styled, { ThemeProvider } from "styled-components";
    import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
    import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
    import { GlobalStyles } from "styles/globalStyles";
    import Header from "components/common/Header";
    import StyledComponentsRegistry from "../lib/registry";
    
    function RootLayout({
      // Layouts must accept a children prop.
      // This will be populated with nested layouts or pages
      children,
    }: {
      children: React.ReactNode;
    }) {
      const queryClient = new QueryClient();
    
      return (
        <html lang="kr">
          <body>
            <div id="portal" />
            <QueryClientProvider client={queryClient}>
              <ReactQueryDevtools />
              <StyledComponentsRegistry>
                <ThemeProvider theme={theme}>
                  <GlobalStyles />
                  <Applayout>
                    <Header />
                    {children}
                  </Applayout>
                </ThemeProvider>
              </StyledComponentsRegistry>
            </QueryClientProvider>
          </body>
        </html>
      );
    }
    
    const Applayout = styled.div`
      ...css
    `;

    여기서 "use client" 는 server component를 사용하지 않는다고 선언하는것인데, (https://beta.nextjs.org/docs/rendering/server-and-client-components#when-to-use-server-vs-client-components)

    이곳에 언제 server component를 사용하면 좋을지, 언제 client component를 사용하면 좋을지 나와있다.

    개인적인 생각으로 아직 server component를 지원하는 라이브러리가 많지 않기 때문에, 정말 가벼운 컴포넌트를 제외하면 거의 클라이언트 컴포넌트를 사용해야 할 것 같다.

     

    3단계 : 기존 page 폴더를 app 폴더로 이동

    appDir 에서 라우트 방식이 조금 바뀌었기 때문에 page 폴더 그대로 옮겨선 안되고, 약간의 수정이 있어야 한다.

    page에서 사용하던 index.tsx 파일들을 page.tsx로 변경한다.

     

    index.tsx -> page.tsx | Route = "/"

    about.tsx -> about/page.tsx | Route = "/about"

    blog/[slug].tsx -> blog/[slug]/page.tsx | Route = "/blog/123"

     

    이때 우선 클라이언트 컴포넌트 선언을 한 뒤에 변경하면 에러가 많이 줄어들 것이다.

     

    4단계 : 기존 useRoute form "next/router" 을 새로운 useRoute from "next/navigation" 으로 변경한다

     두 useRouter은 다른 hook 이며 기존의 useRouter은 appDir에서 동작하지 않는다.

    'use client';
    
    import { useRouter, usePathname, useSearchParams } from 'next/navigation';
    
    export default function ExampleClientComponent() {
      const router = useRouter();
      const pathname = usePathname();
      const searchParams = useSearchParams();
    
      // ...
    }

    3가지의 훅스가 추가되었는데 각각의 역할은

    1. useRouter : push(), replace(), refrash(), prefetch(), back(), forword() 등의 라우터 변경을 담당한다.

    2. usePathname : 현재 url 의 경로를 문자열로 리턴한다.

    3. useSearchParams : 쿼리스트링을 읽는데 쓰인다.

     

    5단계 : 기존 data fetch 방식을 마이그레이션 한다.

    getServerSideProps, getStaticProps 를 fetch()로 변환한다.

    function Page({ data }) {
      // Render data...
    }
    
    
    export async function getServerSideProps() {
      const res = await fetch(`https://.../data`)
      const data = await res.json()
    
      return { props: { data } }
    }
    
    export default Page

    위 코드를 아래와 같이 바꿀 수 있다.

    async function getProjects() {
      const res = await fetch(`https://...`, { cache: 'no-store' });
      const projects = await res.json();
    
      return projects;
    }
    
    export default async function Dashboard() {
      const projects = await getProjects();
    
      return projects.map((project) => <div>{project.name}</div>);
    }

     

    'Web > Next.JS' 카테고리의 다른 글

    Next.js) Link를 쓸까? router.push()를 쓸까?  (1) 2022.01.11
Designed by Tistory.