-
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