Next.js 13버전은 Next.js의 역사를 통틀어서 가장 큰 변화가 있었다. 서버사이드 렌더링의 구조에 변화가 많은 리액트 18을 채용하였으며 레이아웃 지원또한 본격적으로 지원하기 시작했다. 또한 바벨을 대체할 러스트기반 SWC를 뒤이어 웹팩을 대체할 Turbopack까지 출시했다!
app 디렉터리
Next.js의 아쉬운 점으로 평가받던 것 중 하나로 레이아웃의 존재가 항상 지목되어 왔었다.
공통 헤더와 공통 사이드바가 대부분의 페이지에 필요한 웹사이트를 만든다고 생각해보자.
만약 react-router-dom을 사용한다면 아래와 같은 방식으로 라우팅을 만들어 볼 수 있다.
import { Route, Routes } from 'react-router-dom';
const App = () => {
return (
<div>
<div>외부의 공통 영역</div>
<Routes>
<Route path="/" element={<Layout/>}>
<Route index element = {<Home/>}></Route>
<Route path="menu1" element={<Menu1/>}></Route>
<Route path="menu2" element={<Menu2/>}></Route>
</Route>
</Routes>
</div>
);
}
export default App;
위처럼 Routes를 활용하여 주소가 바뀌어도 공통 영역을 지정해주는것이 가능하다.
(Routes의 Outlet을 활용하여 하위주소를 렌더링 하는 것또 한 가능하니 참고해보면 좋을 것 같다)
https://reactrouter.com/en/main/components/outlet
아무튼! 이번 글은 Next.js를 설명하는 것이니 이정도로 하고 기존 Next.js에서는 위와 같은 구조를 어떻게 짰을까?
13버전 이전까지 Next.js의 모든 파일은 물리적으로 구별된 파일에 독립되어 있었고 공통으로 무언가를 넣을 수 있는 곳은 _document , _app이 유일했다. 하지만 해당 파일들은 아래와 같은 목적성을 가진 파일들이다.
_document : 페이지에 사용되는 html,body 태그를 수정하거나 CSS-in-JS를 지원하기 위한 코드를 넣는 등의 제한적 용도로만 사용된다.
_app : 페이지를 초기화 하기 위한 용도로 사용되며 아래와 같은 작업이 가능하다고 명시되어 있다.
- 페이지 변경시에 유지하고 싶은 레이아웃
- 페이지 변경 시 상태 유지
- componentDidCatch를 활용한 에러 핸들링
- 페이지간 추가적인 데이터 삽입
- global CSS 삽입
즉, Nextx.js에서 공통 레이아웃을 유지하려면 _app이 유일하였으며 이 역시 제한적이었으며 각 페이지벼롤 다른 레이아웃을 유지하기는 힘들었다. 13버전에서 app 레이아웃이 등장하며 해당 문제를 극복할 수 있게 되었다.
라우팅
기존에 /pages로 정의하던 라우팅 방식이 /app 디렉터리로 이동하며 파일명으로 라우팅하는 것이 불가능해 졌다.
기본적으로 Next.js의 라우팅은 파일 시스템을 기반으로 하고 있다. 이번에 등장한 /app라우팅은 /pages와는 약간의 차이가 있다.
1. /pages/a/b.tsx or /pages/a/b/index.tsx는 모두 동일한 주소로 변환된다. (파일명이 index라면 내용이 무시된다.)
2. /app/a/b는 /a/b로 변환되며 파일명은 무시되게 된다.
즉, app 디렉터리 내부의 파일명은 라우팅에 아무런 영향을 미치지 못한다.
Next.js 13부터는 app 디렉터리 내부의 폴더명이 라우팅이되며 파일명이 제한되어있다.
자! 이제 직접 간단한 next.js 프로젝트를 만들어 보자.
layout.js
layout.js는 페이지의 기본적인 레이아웃을 구성하는 요소이다. 페이지의 기본적인 레이아웃을 구성하며 하위폴더 및 주소에 모두 영향을 미치게 된다.
루트에는 단 하나의 layout을 만들어 둘 수 있으며 모든 페이지에 영향을 미치는 공통 레이아웃이며 웹 페이지를 만드는데 공통적인 내용 (html,head)등을 다룰 수 있다.
이는 _app,_document를 하나로 대체할 수 있는 좋은 사용점이 될 것으로 보이며 꼭 공통 레이아웃이 필요하지 않더라도 기본정보들만 담아둬도 유용할 것 같다.
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>웹페이지에 들어왔다!</body>
</html>
);
}
layout은 주소별 공통 UI를 포함할 수 있을 뿐 아니라 페이지 시작에 필요한 공통 코드를 삽입하는 것 또한 가능하다. 단, 아래 주의점은 생각해두자.
layout은 app디렉터리 내부에서는 예약어이므로 layout 목적 이외에 사용이 불가능하다.
layout은 children을 props로 받아서 렌더링해야한다.
layout 내부에는 export default가 있어야 한다.
layout내부에서 API요청이 가능하다.
잘 이해가 안된다면 아래와 같이 폴더 구조를 만들어보자
page
page또한 예약어이며 일반적으로 다루는 페이지를 의미한다. 위 폴더구조의 layout과 page를 아래와 같이 써보자.
//layout
import { ReactNode } from "react";
export default function BlogLayout({ children }: { children: ReactNode }) {
return <section>{children}</section>;
}
//page
export default function BlogPage() {
return <>여기에 블로그 글</>;
}
이후 이를 테스트해보면 아래와 같이 나온다. layout이 잘 적용되어 브로그 글이라 써진 부분이 section태그로 감싸져 있다.
page는 props를 받을 수 있다.
1. params : [...id]와 같은 동적 라우트 파라미터를 가져올 수 있다.
2. searchParams : URL의 ?a=1과 같은 URLSearchParams를 가져올 수 있다. 이 값은 layout에서는 제공되지 않으니 주의하자.
error.js
error.js는 해당 라우팅 영역에서 사용되는 공통 에러 컴포넌트이다. 이를 활용하여 특정 라우팅별로 다른 에러 UI를 렌더링할 수 있다.
import { useEffect } from "react";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
useEffect(() => {
console.log("logging error", error);
}, [error]);
return (
<div>
<h1>Error: {error.message}</h1>
<button onClick={() => reset()}>Reset</button>
</div>
);
}
error 페이지는 에러 정보를 담고 있는 error:Error객체와 에러 바운더리를 초기화할 reset을 props로 받는다. 해당 에러 바운더리는 클라이언트에서만 작동하여 error컴포넌트도 클라이언트 컴포넌트여야 하며 같은 layout에서 에러가 발생한다면 해당 error컴포넌트로 이동하지 않게 된다.
<Layout>
<Error>
{children}
</Error>
</Layout>
이는 위 구조처럼 페이지가 렌더링 되기 때문이며 Layout의 에러를 처리하려면 상위 컴포넌트의 error을 사용해야 한다.
not-found.js
404 페이지를 렌더링할 때 사용된다.
export default function NotFound() {
return (
<>
<h1>없지롱~~</h1>
</>
);
}
loading.tsx
다음 글에서 자세하게 알아볼 Suspense를 기반으로 컴포넌트가 불러오는 중임을 나타내는 경우 사용할 수 있다.
export default function Loading() {
return "Loading";
}
route.js
app디렉터리가 정식 출시되며 /pages/api에 대한 지원도 추가되었다.
/app/api를 기준으로 디렉터리 라우팅을 지원하며 위에서 설명한 것처럼 /api에도 파일명 라우팅이 사라졌으며 디렉터리가 라우팅 주소를 담당하며 파일명은 route.js로 통일되었다.
import { NextRequest } from "next/server";
export async function GET(request: Request) {}
export async function POST(request: Request) {}
export async function PUT(request: Request) {}
export async function PATCH(request: Request) {}
export async function DELETE(request: Request) {}
export async function OPTIONS(request: Request) {}
export async function HEAD(request: Request) {}
route.ts파일 내부에 RESTAPI메서드명을 예약어로 선언해두면 HTTP요청에 맞게 해당 메서드를 호출하게 된다.
약간 주의할 점은 app/api 이외에서 선언해도 작동한다. 이렇게 라우팅 명칭에 자유도가 생긴 대신에 route.ts의 폴더 내부에는 page.tsx가 존재할 수 없다.
//app/api/users/[id]/route.ts
import { NextRequest } from "next/server";
export async function GET(
request: NextRequest,
context: { params: { id: string } }
) {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${context.params.id}`
);
return new Response(JSON.stringify(response), {
status: 200,
headers: {
"content-type": "application/json",
},
});
}
이후 간단하게 api요청을 보내면
curl -X GET "http://localhost:3004/users/2"
위와같이 api가 잘 오게 된다.
'FrontEnd > Deep Dive' 카테고리의 다른 글
[React] Deep Dive 모던 리액트(30) Next.js 13 & 리액트 18 (1) | 2024.02.18 |
---|---|
[React] Deep Dive 모던 리액트(29) Next.js 서버 컴포넌트 (0) | 2024.02.17 |
[React] Deep Dive 모던 리액트(27) 리액트 18 변경점 (0) | 2024.02.12 |
[React] Deep Dive 모던 리액트(26) 리액트 18 추가된 훅 (1) | 2024.02.07 |
[React] Deep Dive 모던 리액트(25) 리액트 17 변경점 (2) | 2024.02.06 |