diff --git "a/week-06/\352\271\200\353\257\274\355\230\201.md" "b/week-06/\352\271\200\353\257\274\355\230\201.md" new file mode 100644 index 0000000..578e93e --- /dev/null +++ "b/week-06/\352\271\200\353\257\274\355\230\201.md" @@ -0,0 +1,849 @@ +# Next.js 13과 리액트 18 + +Next.js 13은 기존 Next.js와 비교했을 때 변화가 큰 버전이다. + +가장 큰 변화는 다음과 같다. + +* App Router 도입 +* React Server Component 지원 +* Streaming SSR 지원 +* `layout.tsx`, `page.tsx`, `loading.tsx`, `error.tsx` 같은 파일 기반 규칙 추가 +* SWC, Turbopack 등 빌드 도구 개선 + +기존 React 애플리케이션은 브라우저에서 대부분의 렌더링을 처리하는 방식에 가까웠다. + +하지만 React 18과 Next.js 13부터는 서버와 클라이언트가 각자 잘할 수 있는 역할을 나누는 방향으로 변화했다. + +즉, 모든 컴포넌트를 브라우저로 내려보내는 것이 아니라, 서버에서 처리해도 되는 부분은 서버에서 처리하고, 사용자 인터렉션이 필요한 부분만 클라이언트에서 처리하는 구조로 바뀌고 있다. + +--- + + +# App Router + +## layout.tsx + +App Router에서는 `layout.tsx`를 통해 공통 UI를 유지할 수 있다. + +```tsx +// app/layout.tsx +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + +
공통 헤더
+ {children} + + + ); +} + +``` + +위 코드에서 `header`는 여러 페이지에서 공통으로 유지된다. + +페이지가 변경되어도 레이아웃은 유지되고, `children`에 해당하는 페이지 영역만 변경된다. + +이 구조는 페이지 전환 시에도 유지되어야 하는 UI를 표현하기 좋다. + +--- + +## Params와 SearchParams + +App Router의 `page.tsx`에서는 `params`와 `searchParams`를 받을 수 있다. + +```tsx +export default function Page({ + params, + searchParams, +}: { + params: { id: string }; + searchParams: { keyword?: string }; +}) { + return ( +
+

id: {params.id}

+

keyword: {searchParams.keyword}

+
+ ); +} + +``` + +### Params + +`params`는 동적 라우팅에서 사용되는 값을 의미한다. + +예를 들어 다음과 같은 폴더 구조가 있다고 해보자. + +```txt +app/ + posts/ + [id]/ + page.tsx + +``` + +이 경우 `/posts/1`에 접근하면 `params.id`는 `"1"`이 들어온다. + +```tsx +export default function Page({ + params, +}: { + params: { id: string }; +}) { + return
{params.id}
; +} + +``` + +즉, `params`는 URL 경로 안에 포함된 동적인 값을 가져올 때 사용한다. + +```txt +/posts/1 + ↑ + params.id + +``` + +또한, 점 세개('...')를 사용해서 모든 path를 잡을 수 있다. + +즉, `[...id]`처럼 catch-all route를 사용하는 경우에는 여러 경로 값을 배열로 받을 수 있다. + +```txt +app/ + posts/ + [...id]/ + page.tsx + +``` + +예를 들어 `/posts/a/b`로 접근하면 `params.id`에는 다음과 같은 값이 들어온다. + +```ts +{ + id: ['a', 'b'] +} + +``` + +### SearchParams + +`searchParams`는 URL의 query string 값을 의미한다. + +query string은 URL에서 `?` 뒤에 붙는 값이다. + +```txt +/posts?keyword=react&page=1 + ↑ + searchParams + +``` + +위 URL에서 `searchParams`는 다음과 같은 값을 가진다. + +```ts +{ + keyword: 'react', + page: '1' +} + +``` + +따라서 `page.tsx`에서는 다음과 같이 사용할 수 있다. + +```tsx +export default function Page({ + searchParams, +}: { + searchParams: { + keyword?: string; + page?: string; + }; +}) { + return ( +
+

검색어: {searchParams.keyword}

+

페이지: {searchParams.page}

+
+ ); +} + +``` + +### searchParams는 layout에서 제공되지 않는다 + +`searchParams`는 `page.tsx`에서는 사용할 수 있지만, `layout.tsx`에서는 제공되지 않는다. + +그 이유는 `layout`은 페이지 탐색 중에는 리렌더링을 수행하지 않기 때문이다. + +App Router에서 `layout.tsx`는 여러 페이지에서 공통으로 사용하는 UI를 담당한다. + +예를 들어 공통 헤더나 사이드바처럼 페이지가 바뀌어도 유지되어야 하는 영역에 사용한다. + +여기서 `layout.tsx`는 한 번 렌더링된 뒤, 하위 페이지가 변경되어도 그대로 유지될 수 있다. + +즉, 페이지가 이동할 때마다 layout이 항상 다시 렌더링되는 구조가 아니다. + +반면 `searchParams`는 URL의 query string에 따라 자주 바뀔 수 있는 값이다. + +예를 들어 다음 두 URL은 같은 페이지를 가리키지만 query string만 다르다. + +```txt +/posts?keyword=react +/posts?keyword=nextjs + +``` + +이때 검색어만 바뀌었을 뿐, 전체 레이아웃을 다시 렌더링할 필요는 없다. + +만약 `layout`이 `searchParams`에 의존하게 되면 query string이 바뀔 때마다 공통 레이아웃까지 다시 계산해야 할 수 있다. + +그래서 Next.js는 `searchParams`를 `layout`에 제공하지 않는다. + +--- + + +# Server Component + +## Server Component와 Client Component + +App Router에서 가장 중요한 개념 중 하나는 Server Component와 Client Component다. + +Next.js App Router에서는 기본적으로 컴포넌트가 Server Component로 동작한다. + +```tsx +// app/page.tsx +export default function Page() { + return
Hello Next.js
; +} + +``` + +위 컴포넌트는 별도의 설정이 없다면 Server Component다. + +Server Component는 서버에서 실행되는 컴포넌트다. + +따라서 브라우저에서 실행되어야 하는 코드, 예를 들어 `useState`, `useEffect`, `onClick`, `window`, `localStorage` 같은 기능을 사용할 수 없다. + +```tsx +export default function Page() { + return ; +} + +``` + +위 코드는 Server Component에서 사용할 수 없다. + +이벤트 핸들러는 브라우저에서 실행되어야 하기 때문이다. + +이럴 때는 파일 상단에 `'use client'`를 작성해야 한다. + +```tsx +'use client'; + +import { useState } from 'react'; + +export default function Counter() { + const [count, setCount] = useState(0); + + return ( + + ); +} + +``` + +`'use client'`를 작성하면 해당 컴포넌트는 Client Component가 된다. + +--- + +## Server Component는 언제 사용할까? + +Server Component는 서버에서 처리할 수 있는 일을 담당한다. + +예를 들어 다음과 같은 작업에 적합하다. + +* 데이터 조회 +* DB 접근 +* 서버에서만 필요한 로직 처리 +* 정적인 UI 렌더링 + +```tsx +export default async function Page() { + const posts = await getPosts(); + + return ( + + ); +} + +``` + +App Router에서는 컴포넌트 자체를 `async`로 만들 수 있다. + +이를 통해, 서버에서 데이터를 가져온 뒤 바로 JSX를 반환할 수 있다. + +```tsx +export default async function Page() { + const posts = await getPosts(); + + return ; +} + +``` + +--- + +## Server Component는 어떻게 브라우저에 전달될까? + +Server Component는 서버에서 실행되는 컴포넌트다. + +그렇다면 서버에서 실행된 컴포넌트의 결과는 어떤 형태로 브라우저에 전달될까? + +Server Component는 서버에서 실행된 뒤, 클라이언트가 React 컴포넌트 트리를 구성할 수 있는 **데이터 형태**로 변환된다. + +이 과정을 **직렬화**라고 한다. + +**직렬화**는 객체나 데이터 구조를 네트워크로 전달할 수 있는 형태로 바꾸는 과정이다. + +반대로 브라우저는 서버에서 받은 데이터를 다시 React가 이해할 수 있는 형태로 되돌린다. + +이 과정을 **역직렬화**라고 한다. + +--- + +즉, Server Component는 서버에서 HTML 문자열만 만들어서 보내는 것이 아니다. + +클라이언트가 React 트리를 다시 구성할 수 있도록 필요한 정보를 데이터 형태로 전달한다. + +이때 서버에서 렌더링할 수 있는 Server Component는 렌더링 결과를 직렬화해서 전달한다. + +반면 Client Component로 표시된 부분은 서버에서 직접 실행할 수 없기 때문에, 해당 위치를 비워두고 참조 정보만 전달한다. + +```txt +Server Component +→ 서버에서 렌더링한 결과를 데이터로 전달 + +Client Component +→ 서버에서 직접 렌더링하지 않고, 클라이언트 번들에서 찾을 수 있도록 참조 정보 전달 + +``` + +--- + +### Wire Format + +Server Component의 렌더링 결과는 일반적인 JSON처럼 사람이 읽기 좋은 형태라기보다는, React가 이해할 수 있는 특수한 데이터 형식으로 전달된다. + +이런 데이터 형식을 **와이어 포맷**이라고 한다. + +와이어 포맷은 서버와 클라이언트가 서로 데이터를 주고받기 위해 약속한 전송 형식이라고 볼 수 있다. + +```txt +서버 +→ React 컴포넌트 트리 정보를 와이어 포맷으로 변환 +→ 클라이언트에 스트리밍 + +클라이언트 +→ 와이어 포맷을 해석 +→ React 컴포넌트 트리 구성 + +``` + +와이어 포맷에는 `M`, `S`, `J` 같은 표시가 등장한다. + +각 문자는 대략 다음과 같은 의미로 이해할 수 있다. + +```txt +M +→ Client Component를 의미한다. +→ 클라이언트 번들에서 해당 컴포넌트를 찾기 위한 참조 정보를 담는다. + +S +→ Suspense를 의미한다. +→ 아직 준비되지 않은 영역이나 비동기 렌더링 경계를 나타낸다. + +J +→ Server Component의 렌더링 결과를 의미한다. +→ element, className, props, children 등 화면을 구성하는 데 필요한 정보가 들어 있다. + +``` + +여기서 중요한 점은 Client Component가 서버에서 완전히 렌더링되어 전달되는 것이 아니라는 점이다. + +Client Component는 서버에서 실행할 수 없다. + +따라서 서버는 Client Component가 들어갈 자리에 실제 렌더링 결과를 넣는 대신, 클라이언트에서 해당 컴포넌트를 찾을 수 있는 참조를 남긴다. + +예를 들어 `@1`, `@2`와 같은 값은 특정 위치에 어떤 참조가 들어가야 하는지를 나타내는 표시라고 볼 수 있다. + +```txt +@1 +: M1에 해당하는 Client Component가 준비되면 이 위치에 연결한다. + +@2 +: M2에 해당하는 다른 참조가 이 위치에 들어간다. + +``` + +즉, 서버는 모든 것을 완성된 HTML로만 보내는 것이 아니라, 클라이언트가 React 컴포넌트 트리를 구성하는 데 필요한 정보를 경제적인 형식으로 전달한다. + +이 구조 덕분에 서버에서 처리할 수 있는 부분은 서버에서 끝내고, 클라이언트에서 필요한 부분만 JavaScript 번들을 통해 실행할 수 있다. + +## Server Component와 SSR은 다른 개념이다 + +Server Component를 처음 보면 SSR과 헷갈릴 수 있다. + +둘 다 서버가 관여한다는 점에서는 비슷해 보인다. + +하지만 목적과 동작 방식이 다르다. + +SSR은 서버에서 HTML을 만들어 브라우저에 전달하는 방식이다. + +```txt +서버에서 HTML 생성 +↓ +브라우저에 HTML 전달 +↓ +브라우저에서 JavaScript 로드 +↓ +Hydration (서버에서 미리 만들어진 HTML에 React의 JavaScript 기능을 연결하는 과정) +↓ +사용자 interaction 가능 + +``` + +SSR의 핵심은 초기 HTML을 서버에서 만들어 빠르게 보여주는 것이다. + +반면 Server Component는 컴포넌트 자체를 서버에서 실행하고, 클라이언트에 필요한 결과만 전달하는 개념이다. + +Server Component는 클라이언트 JavaScript 번들에 포함되지 않을 수 있다. + +따라서 브라우저로 전달되는 JavaScript 양을 줄이는 데 도움이 된다. + +정리하면 다음과 같다. + +```txt +SSR +→ 초기 HTML을 서버에서 만들어 보내는 렌더링 방식 + +Server Component +→ 서버에서만 실행될 수 있는 React 컴포넌트 모델 + +``` + +둘은 대체 관계가 아니다. + +같이 사용될 수 있는 개념이다. + +--- + +## Hydration + +Hydration은 서버에서 만들어진 HTML에 React의 이벤트와 상태를 연결하는 과정이다. + +SSR을 사용하면 서버에서 HTML을 먼저 만들어 브라우저에 전달한다. + +하지만 HTML만으로는 React의 이벤트 핸들러가 동작하지 않는다. + +```tsx + + +``` + +서버에서 위 버튼의 HTML을 만들어 보낼 수는 있다. + +하지만 `onClick` 같은 이벤트는 브라우저에서 JavaScript가 실행되어야 동작한다. + +따라서 브라우저는 서버에서 받은 HTML에 React 코드를 연결해야 한다. + +이 과정을 Hydration이라고 한다. + +```txt +서버에서 HTML 전달 +↓ +브라우저가 HTML을 먼저 표시 +↓ +React JavaScript 다운로드 및 실행 +↓ +HTML과 React 상태, 이벤트 연결 + +``` + +Hydration이 완료되기 전에는 화면은 보이지만, 일부 인터렉션은 아직 동작하지 않을 수 있다. + +--- + + +# Streaming SSR + +기존 SSR에서는 서버가 페이지에 필요한 모든 데이터를 준비한 뒤 HTML을 내려주는 방식에 가깝다. + +```txt +모든 데이터 준비 -> HTML 생성 -> 브라우저 전달 + +``` + +이 방식은 느린 데이터 요청이 하나라도 있으면 전체 페이지 응답이 늦어질 수 있다. + +React 18에서는 Streaming SSR을 통해 준비된 부분부터 먼저 브라우저에 보낼 수 있다. + +```txt +빠르게 준비된 영역 먼저 전달 +↓ +느린 영역은 fallback UI 표시 +↓ +데이터가 준비되면 나중에 이어서 전달 + +``` + +Next.js App Router에서는 `loading.tsx`를 통해 로딩 UI를 쉽게 만들 수 있다. + +```txt +app/ + posts/ + loading.tsx + page.tsx + +``` + +```tsx +// app/posts/loading.tsx +export default function Loading() { + return
로딩 중...
; +} + +``` + +`page.tsx`에서 데이터를 가져오는 동안 `loading.tsx`가 먼저 보여질 수 있다. + +이 방식은 사용자가 빈 화면을 오래 보는 문제를 줄이는 데 도움이 된다. + +--- + + +# Route Handler + +Pages Router에서는 API를 만들기 위해 `pages/api`를 사용했다. + +```txt +pages/ + api/ + hello.ts + +``` + +```ts +// pages/api/hello.ts +export default function handler(req, res) { + res.status(200).json({ message: 'hello' }); +} + +``` + +App Router에서는 `route.ts` 파일을 사용한다. + +```txt +app/ + api/ + hello/ + route.ts + +``` + +```ts +// app/api/hello/route.ts +export async function GET() { + return Response.json({ message: 'hello' }); +} + +``` + +HTTP 메서드에 따라 `GET`, `POST`, `PUT`, `DELETE` 같은 함수를 export할 수 있다. + +```ts +export async function POST(request: Request) { + const body = await request.json(); + + return Response.json({ + message: 'created', + data: body, + }); +} + +``` + +즉, App Router에서는 API 응답을 만들 때 `app/api/.../route.ts` 구조를 사용한다. + +## 매개변수: Request와 Context + +Route Handler의 함수는 `request`와 `context`를 매개변수로 받을 수 있다. + +```ts +export async function GET( + request: Request, + context: { params: { id: string } } +) { + return Response.json({ + id: context.params.id, + }); +} + +``` + +### Request + +`request`는 클라이언트가 보낸 HTTP 요청 정보를 담고 있다. + +이를 통해 요청 본문(body), 헤더(header), 쿠키(cookie), URL 정보 등을 확인할 수 있다. + +```ts +export async function POST(request: Request) { + const body = await request.json(); + + return Response.json(body); +} + +``` + +위 코드에서는 `request.json()`을 사용해 클라이언트가 보낸 요청 본문을 읽고 있다. + +URL의 query string도 읽을 수 있다. + +```ts +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + + const keyword = searchParams.get('keyword'); + + return Response.json({ + keyword, + }); +} + +``` + +```txt +/api/posts?keyword=react + +``` + +위 요청이 들어오면 다음과 같은 응답을 반환할 수 있다. + +```json +{ + "keyword": "react" +} + +``` + +즉, `request`는 클라이언트가 보낸 요청 데이터를 읽기 위해 사용한다. + +### NextRequest + +Next.js에서는 `Request` 대신 `NextRequest`를 사용할 수도 있다. + +```ts +import { NextRequest } from 'next/server'; + +export async function GET(request: NextRequest) { + const keyword = request.nextUrl.searchParams.get('keyword'); + + return Response.json({ + keyword, + }); +} + +``` + +`NextRequest`는 기본 Web API의 `Request` 객체를 확장한 Next.js만의 요청 객체라고 볼 수 있다. + +기본적인 요청 정보뿐만 아니라, Next.js에서 자주 사용하는 기능을 더 편하게 사용할 수 있도록 제공된다. + +예를 들어 `NextRequest`를 사용하면 `nextUrl`을 통해 URL 정보를 더 쉽게 다룰 수 있다. + +```ts +export async function GET(request: NextRequest) { + const pathname = request.nextUrl.pathname; + const keyword = request.nextUrl.searchParams.get('keyword'); + + return Response.json({ + pathname, + keyword, + }); +} + +``` + +또한 쿠키 정보도 확인할 수 있다. + +```ts +export async function GET(request: NextRequest) { + const token = request.cookies.get('token'); + + return Response.json({ + token, + }); +} + +``` + +따라서 단순히 요청 body나 URL만 확인한다면 `Request`로도 충분하다. + +하지만 쿠키나 `nextUrl`처럼 Next.js에서 제공하는 기능을 사용해야 한다면 `NextRequest`를 사용할 수 있다. + +--- + +### Context + +`context`는 동적 라우트의 `params`만 제공한다. + +페이지 컴포넌트의 `params`와 비슷한 개념이라고 생각하면 이해하기 쉽다. + +예를 들어 다음과 같은 구조가 있다고 해보자. + +```txt +app/ + api/ + posts/ + [id]/ + route.ts + +``` + +사용자가 다음 URL로 요청을 보낸 경우 + +```txt +/api/posts/1 + +``` + +`context.params.id`에는 `"1"`이 들어온다. + +```ts +export async function GET( + request: Request, + { params }: { params: { id: string } } +) { + return Response.json({ + id: params.id, + }); +} + +``` + +--- + + +# Error 처리 + +App Router에서는 에러 처리를 파일 기반으로 할 수 있다. + +대표적으로 `error.tsx`를 사용할 수 있다. + +```txt +app/ + dashboard/ + error.tsx + page.tsx + +``` + +```tsx +'use client'; + +export default function Error() { + return
문제가 발생했다.
; +} + +``` + +`error.tsx`는 해당 route segment에서 발생한 에러를 처리하는 Error Boundary 역할을 한다. + +주의할 점은 `error.tsx`는 Client Component여야 한다. + +따라서 파일 상단에 `'use client'`를 작성해야 한다. + +404 페이지는 `not-found.tsx`로 처리할 수 있다. + +```tsx +// app/not-found.tsx +export default function NotFound() { + return
페이지를 찾을 수 없습니다.
; +} + +``` + +특정 상황에서 의도적으로 404 페이지를 보여주고 싶다면 `notFound()`를 사용할 수 있다. + +```tsx +import { notFound } from 'next/navigation'; + +export default async function Page() { + const data = await getData(); + + if (!data) { + notFound(); + } + + return
{data.title}
; +} + +``` + +--- + + +# Next.js 16에서 기본 번들러가 된 Turbopack + +Turbopack은 Next.js에서 사용하는 Rust 기반 번들러다. + +번들러는 여러 JavaScript, TypeScript, CSS 파일을 브라우저에서 실행할 수 있는 형태로 묶어주는 도구다. + +기존 Next.js에서는 Webpack이 대표적인 번들러 역할을 했다. + +하지만 애플리케이션 규모가 커질수록 개발 서버 시작 시간이나 코드 변경 후 반영 속도에서 한계가 생길 수 있다. + +Turbopack은 이러한 문제를 개선하기 위해 만들어졌다. + +```txt +Webpack +: 생태계와 플러그인 호환성이 강점 + +Turbopack +: 빠른 개발 서버, 빠른 코드 반영, 빌드 성능 개선을 목표 + +``` + +--- + +Next.js 16부터 Turbopack은 stable 상태가 되었고, `next dev`와 `next build`에서 기본 번들러로 사용된다. + +즉, 예전처럼 Turbopack을 사용하기 위해 별도의 `--turbo` 또는 `--turbopack` 옵션을 붙일 필요가 없어졌다. + +Turbopack은 단순히 Webpack보다 빠른 실험적 도구라기보다, Next.js가 기본적으로 선택하는 번들러 방향에 가깝다. + +Turbopack의 핵심은 변경된 부분을 빠르게 추적하고, 필요한 부분만 다시 처리하는 데 있다. + +```txt +코드 수정 +↓ +변경된 부분 추적 +↓ +필요한 부분만 다시 처리 +↓ +빠른 반영 +``` + +이를 통해 개발 중 코드 수정 후 화면에 반영되는 속도와 production build 속도를 개선할 수 있다.