이번 글은 Next.js 14 버전 기준으로 글을 쓰게 되었다. 이전 버전과 대비해서 다양한 변화가 있었고
전 회사에서 Next.js 12~13 버전만 사용했었던 경험이 있어 빠르게 변화되는 Next js에 대해 공부하고 기록 하고 싶었다.
파일 시스템 기반 라우팅은 Next.js의 핵심 기능 중 하나이다.
이전 버전까지는 pages라는 디렉토리 아래에 폴더와 파일을 규칙에 맞게 넣어 라우팅 구조를 만들 수 있었지만 Next.js 14 버전 이후에는 app 이라는 디렉토리로 만드는 라우팅도 함께 지원되고 있다.
Next js Routing 공식문서
app 이라는 폴더를 만들고 그 아래 dashobard, settings 라는 폴더를 만들면 URL path가 폴더 이름과 동일한 순서를 따라 만들어진다.
위의 이미지를 봤을 때 표면적으로 pages와 app은 크게 달라지지 않았지만 실제로 App Router는 React 18의 React Server Component(RSC), Suspense를 기본적으로 염두한 방식이다.
// app/page.tsx
export default function Page() {
return <h1>Hello, Next.js!</h1>;
}
// app/layout.tsx
export default function Layout({ children }: { children: React.ReactNode }) {
return <section>{children}</section>;
}
// app 내부에 page.tsx 과 layout.tsx 파일을 만든다.
// 파일 시스템을 통해 레이아웃을 정의할 수 있고 레이아웃은 여러 페이지 간에 UI를 공유 할 수 있다.
// 즉 Layout에 다른 컴포넌트들이 props로 들어오기 때문에 공통으로 UI를 적용 할 수 있다.
app 디렉토리를 사용하면 상태를 유지하고 비용이 많이 드는 re-render를 방지하며 복잡한 인터페이스를 쉽게 배치 할 수 있고 레이아웃을 중첩하고 구성 요소, 테스트, 스타일과 같은 경로와 애플리케이션 코드를 같은 위치에 배치할 수 있다.
Segment에는 파일이 필요한데, Next.js 에서는 상황에 맞는 UI를 정의할 때 쓰는 파일명이 미리 정해져있다.
Link 컴포넌트는 Next js를 써본 사람이라면 이미 다들 알고 있는 부분이지만 기록겸 새로 알게된 내용도 추가해서 적어 보겠다.
Link 컴포넌트는 HTML a 태그의 확장된 버전이고, Link로 이동하게 되면 최초 실행은 SSG로 실행되지만 해당 페이지에 대한 라우팅 정보와 json 파일(data)정보를 미리 가져와 CSR 방식으로 빠르게 라우팅 할 수 있다.
그리고 Next js 12에서는 Link가 a 태그를 완전히 대체하지 못했기 때문에 Link의 자식으로 a 태그를 넣는걸 권장했지만 13에서는 a 태그가 필요하지 않게 되었다.
import Link from 'next/link'
export default function Page() {
return <Link href="/dashboard">Dashboard</Link>
}
// v12
import { useRouter } from 'next/router'
export default function ReadMore() {
const router = useRouter()
return (
<button onClick={() => router.push('/about')}>
Click here to read more
</button>
)
}
// v13~
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button type="button" onClick={() => router.push('/dashboard')}>
Dashboard
</button>
)
}
useRouter를 사용할 때 큰 차이는 없지만 import 할 때 next/router => next/navigation 차이가 있다.
그리고 'use client'라는 코드가 보이는데 이 부분은 중요한 부분이니 밑에 가서 자세하게 설명 하겠다.
Next.js 14은 React 18 에서 나온 RSC(React Server Component)에 대한 개념을 염두해두면서 만들었기 때문에 Server Components에 대한 이해가 필요하다!
React는 서버에서 렌더링 할 수 있는 Component인 RSC를 제공함으로써 개발자가 원하는 곳에서 컴포넌트를 렌더링 할 수 있는 선택지를 제공한다.
Next js Rendering 공식문서
기존에 사용하던 SSR(Server Side Rendering)은 서버에서 페이지 단위로 정적인 리소스를 생성하지만 RSC는 컴포넌트 단위로 정적인 리소스를 생성할 수 있다.
서버에서 렌더링하게 되면 결과를 캐싱하고 다음 요청에 대해 수행되는 렌더링 및 데이터를 가져오는 상황에서 데이터 가져오는 양이 줄어들게 되어 성능이 향상되고 비용을 절감할 수 있다.
클라이언트로 내려보내는 Js 번들 크기를 줄일 수 있게 되었고 DB와 가까운 곳에서 데이터를 조회하기 때문에 속도가 더 증가한다.
2.1에서 나왔던 이점 덕분에 Next.js 13 에서는 RSC를 기본 컴포넌트 렌더링 방식으로 채택하고 있지만, 모든 컴포넌트를 RSC로 만들 순 없다!!
// 클라이언트 컴포넌트
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}
다시 한 번 언급하면 Next.js 13 버전에서는 RSC를 기본 컴포넌트 렌더링 방식을 채택하고 있어 기본적으로 컴포넌트를 생성하면 Server Component로 만들어지게 된다. 하지만 브라우저 API, useState와 같은 hook을 이용해야하는 경우에는 Client Component를 사용해야한다. 그렇다면 어떻게 해야 Client Component라고 정의할 수 있는지 알아보자!
위에 코드를 보면 최상단에 'use client' 라는 부분이 보일텐데 이것이 바로 Client Component를 사용하겠다 라는 의미이다.
되게 간단하면서도 어색할 수 있지만 알아두어야 한다!
한 눈에 보기 좋게 표로 만들어 보았고, 백엔드와 관련된 부분을 제외한 나머지는 클라이언트 컴포넌트에서만 사용이 가능해졌다.
또한 서버, 클라이언트 컴포넌트가 생겨남으로써 컴포넌트를 설계 및 생성할 때 조심해야 될 부분이 생겨났다.
Next.js 공식문서에서 말하기를 클라이언트 Javscript 번들 크기를 줄이려면 클라이언트 컴포넌트를 트리의 말단으로 보내는게 좋다고 한다.
이해가 안될 수도 있으니 코드로 알아보자
// 1번
// 이 패턴은 동작하지 않는다.
'use client'
import ExampleServerComponent from './example-server-component'
export default function ExampleClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ExampleServerComponent />
</>
)
}
// 2번
'use client'
import { useState } from 'react'
export default function ExampleClientComponent({
children, // 서버 컴포넌트를 자식 요소로 전달할 수 있습니다.
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children}
</>
)
}
클라이언트 컴포넌트를 최대한 말단으로 내려보내라는 건 서버 컴포넌트에서 클라이언트 컴포넌트를 불러오는 동작은 문제가 없지만, 클라이언트 컴포넌트에서 서버 컴포넌트를 불러오는 패턴은 지원되지 않으므로 에러가 발생한다. 그렇기 때문에 클라이언트 컴포넌트를 트리의 말단으로 보내게 되면 다시 말해 컴포넌트들의 집합 중 최하단에 있으면 애초에 1번 같은 예시는 나오지 않기 때문에 조심해야하는 패턴이다.
그리고 만약에 클라이언트 컴포넌트 에서 서버 컴포넌트를 사용하고 싶다면 2번 예시 처럼 직접 import 해서 불러올 순 없지만 children props로 전달 할 수 있다.