Next.js 13의 공식 문서
Next.js 13을 사용한 이유
작년부터 Next.js가 완전 새로운 방식으로 라우팅을 한다는 소식을 들었으나 개발 초창기인 만큼 page구조에서 app 구조로 넘어갈 이유를 느끼지 못했다.
그러나 최근 들어서 공식 문서의 로드맵을 봤을 때 많은 기능이 개발되어 있었으며 개발하는데 참고 할 만한 문서나 프로젝트들의 수가 충분히 쌓였다고 생각했다. 그리고 이미 저번 프로젝트를 진행하면서 Next.js를 사용했기 때문에 이번에는 새로운 것들을 더 공부하고 싶은 생각이 더 강해서 베타버전을 사용하여 진행하기로 결정 하였다.
가장 큰 차이점이라고 한다면 라우팅 구조를 볼 수 있다.
지금까지의 Next.js의 루트 폴더는 pages였으나 이번 버전에서는 app폴더를 루트로 잡는다.
이 app 폴더에는 지난 버전과 마찬가지로 api 폴더가 존재하며 url은 app의 하위 폴더명을 기준으로 url을 설정한다.
이 과정에서 중첩 경로를 설정할 수 있다.
중첩 경로란 만약 app 폴더 하위에 dashboard 폴더가 있고 그 하위에 settings 폴더가 있다면 이 부분의 url은 /dashboard/setting
이 되며 이 url은 /, /dashboard, /dashboard/setting
3개의 페이지로 구성 되어있다는 것을 의미한다.
그리고 Next.js 13에선 부분 렌더링도 할 수 있다.
만약 dashboard라는 같은 부모를 둔 형제간의 경로를 탐색할 때 Next.js는 변경되는 경로의 레이아웃과 페이지만 가져와서 렌더링 한다.
예를 들어 /dashboard
라는 폴더가 있고 각각 /dashboard/settings
와 /dashboard/analytics
라는 형제 폴더가 존재한다면 각각의 형제 폴더를 접근 했을 때 settings 부분만 렌더링 하고 analytics 부분만 렌더링 한다는 것이다.
전체의 화면을 다시 가져오지 않기 때문에 전송되는 데이터 양과 실행시간이 줄어들어 성능이 향상 된다.
고급 라우팅 패턴을 사용하면 병렬 경로, 경로 가로채기, 조건부 경로를 구현 할 수 있다. 각각의 항목에 대해 설명하자면
라우팅에서 이어지는 글이다. 이 규칙대로 파일 이름을 설정 해야 Next.js에서 인식하고 경로를 만들거나 레이아웃을 설정할 수 있다.
pages에서 app으로 넘어가는 마이그레이션을 위해 두개의 폴더를 동시에 사용할 수 있도록 둘 다 지원 하고 있다.
서버 사이드 렌더링을 한다면 상관 없지만 클라이언트 사이드 렌더링을 하게 된다면 “use client”라는 예약어를 파일 제일 상단에 적어서 사용해야 한다.
page.tsx
layout.tsx
export default function Layout({
children, // will be a page or nested layout
}: {
children: React.ReactNode;
}) {
return (
<section>
{/* Include shared UI here e.g. a header or sidebar */}
<nav></nav>
{children}
</section>
);
}
최상위 layout을 루트 레이아웃이라고 하며 애플리케이션의 모든 페이지에서 공유된다. 이 루트 레이아웃에는 html과 body태그를 포함해야한다. (pages의 _documents.tsx의 역할을 대신 한다고 이해하면 된다.)
모든 경로는 자체 레이아웃을 가질 수 있다. 이 레이아웃은 해당 폴더 구조의 모든 페이지에 공유된다.
경로의 레이아웃은 기본적으로 중첩 된다. 각 상위 레이아웃은 하위 레이아웃을 children으로 감싼다.
레이아웃은 기본적으로 서버 사이드로 동작하지만 “use client”를 통해 클라이언트 사이드로 설정할 수 있다.
상위 레이아웃이 하위 레이아웃에 데이터를 전달 할 수 없다.
root layout
//루트 레이아웃
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
템플릿
Next.js 라우터는 클라이언트 측 Navigation과 함께 서버 중심 라우팅을 사용한다. 로딩 UI와 동시 렌더링을 지원한다. 탐색이 클라이언트 측 상태를 유지하고, 큰 비용이 드는 리렌더를 방지하는 효과가 있다.
Link
import Link from "next/link";
export default function Page() {
return <Link href="/dashboard">Dashboard</Link>;
}
import Link from "next/link";
export default function PostList({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
);
}
"use client";
import { useRouter } from "next/navigation";
export default function Page() {
const router = useRouter();
return (
<button type="button" onClick={() => router.push("/dashboard")}>
Dashboard
</button>
);
}
이번 Next.js 13버전에 와서 pages 구조와 제일 크게 달라진 점이라면 서버사이드와 클라이언트 사이드를 표현하는 방식이 달라졌다는 점이다.
기본적으로 app 디렉토리 방식에서는 별도의 표시가 없다면 서버 사이드로 동작하며 서버 사이드로 동작하는 경우 useState나 useEffect와 같이 클라이언트 측에서 사용하는 것들은 사용할 수 없다.
클라이언트 측에서 사용하는 것들은 파일 최상단에 “use client”를 작성하면 사용할 수 있다.
다만 클라이언트 사이드라고 해서 무조건 모든 요소를 클라이언트에게 맡기는 건 아니다.
아래 코드는 “use client”를 파일 상단에 작성해서 클라이언트 사이드로 돌아가도록 했다.
"use client";
const EventDataSection = () => {
const [isEnd, setIsEnd] = useState<boolean>(false);
const isIng = () => {
setIsEnd(false);
};
const isNotIng = () => {
setIsEnd(true);
};
const { translation } = useTranslation();
const { data, isLoading, isFetching, hasNextPage, fetchNextPage } =
useEventQuery({
close: isEnd,
});
const { ref, inView } = useInView({ threshold: 0.05 });
useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage();
}
}, [fetchNextPage, hasNextPage, inView]);
return (
//코드의 대부분은 생략됨
<div className="mt-3 flex w-full justify-end">
<button
className={`mr-4 h-10 w-24 rounded-2xl font-semibold shadow-md hover:bg-secondary-orange active:bg-primary-yellow active:dark:text-text-normal ${
!isEnd
? `bg-secondary-orange dark:text-text-normal`
: `dark:bg-bg-button-dark`
}`}
onClick={isIng}
>
{translation("event.ing")}
</button>
<button
className={`mr-2 h-10 w-24 rounded-2xl font-semibold shadow-md hover:bg-secondary-orange active:bg-primary-yellow active:dark:text-text-normal ${
isEnd
? `bg-secondary-orange dark:text-text-normal`
: `dark:bg-bg-button-dark`
}`}
onClick={isNotIng}
>
{translation("event.end")}
</button>
</div>
);
};
export default EventDataSection;
use client를 사용해서 useState로 상태를 관리해주도록 하였다. 그리고 버튼을 출력하도록 했다.
기본적으로 우리가 아는 클라이언트 사이드라면 저 버튼은 서버에서 받아왔을 때 없어야 정상이다.
하지만 아래 사진을 보면 진행중과 종료라는 버튼 두개가 정상적으로 받아왔다는 것을 알 수 있다.
따라서 기본적으로 서버사이드가 동작하며, 디폴트 데이터 기준 예를들어 useState(false)
이면 false
데이터가 들어간 기준으로 SSR(서버 사이드 렌더링)
을 한 결과물을 next에서 가지고 있다.
"use Client"
를 쓴 컴포넌트는 그 후에 CSR(클라이언트 사이드 렌더링)
로 작동 하는 것이다.
"use client";
// ❌ This pattern will not work. You cannot import a Server
// Component into a Client Component
import ServerComponent from "./ServerComponent";
export default function ClientComponent() {
return (
<>
<ServerComponent />
</>
);
}
// app/ClientComponent.js
"use client";
export default function ClientComponent({ children }) {
return <>{children}</>;
}
// app/page.js
// ✅ This pattern works. You can pass a Server Component
// as a child or prop of a Client Component.
import ClientComponent from "./ClientComponent";
import ServerComponent from "./ServerComponent";
// Pages are Server Components by default
export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
);
}
// app/page.js
import { AcmeCarousel } from "acme-carousel";
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* 🔴 Error: `useState` can not be used within Server Components */}
<AcmeCarousel />
</div>
);
}
“use client”
를 사용해 해당 패키지가 client side로 돌아간다고 감싸주고 export 한다.// app/carousel.js
"use client";
import { AcmeCarousel } from "acme-carousel";
export default AcmeCarousel;
// app/page.js
import Carousel from "./carousel";
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* 🟢 Works, since Carousel is a Client Component */}
<Carousel />
</div>
);
}
병렬 라우팅을 사용하면 하나의 레이아웃에서 하나 이상의 페이지를 동시에 혹은 조건부로 렌더링 할 수 있다. 주로 대시 보드 같이 하나의 페이지에서 여러가지 페이지를 표시 할 때 사용한다.
병렬 라우팅을 사용하면 개별적으로 각 경로에 대해 독립적으로 오류와 로딩 UI를 표시할 수 있다.
조건부로 렌더링도 가능하기 때문에 로그인과 같은 특정 조건에 따라 슬롯을 조건부로 렌더링이 가능하기 때문에 동일한 URL에서 완전히 구분된 코드를 사용할 수 있다.
사용 방법은 아래에 적혀진 대로 사용하면 된다.
const EventLayout = ({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) => {
return (
<>
{children}
{modal}
</>
);
};
export default EventLayout;
경로를 가로채면 현재 페이지를 유지하면서 현재 레이아웃 내에 경로를 불러올 수 있다. 특정 경로를 “가로채기” 하여 다른 경로를 표시하려는 경우에 유용할 수 있다.
예를 들어서 피드 내에서 사진을 클릭하면 모달이 나오게 되는데 이 모달이 나오는 경로를 그 게시물이 있는 주소로 표시할 수 있다. 이경우 새로고침 하면 그 모달이 있던 주소가 아닌 그 게시물이 있는 주소로 이동 하게 된다. 만약 피드를 공유하게 되는 경우 어떤 피드를 공유할지 알 수 없다. 하지만 모달을 만들고 경로를 가로채서 그 게시물의 경로를 띄우는 경우 그 URL을 공유하게 되었을 때 다른 사람은 공유 받은 그 게시물을 볼 수 있게 된다.
가로채기 경로의 경우 파일 앞에 (..) 와 같은 규칙을 사용하여 나타낼 수 있다.
베타버전때 프로젝트 빠르게 완성하고 나는 이렇게 최신 트렌드 따라가는 사람이라면서 자랑하려고 했는데 그새를 못참고 13.4버전이 나오면서 베타가 stable 버전으로 바뀌었다.
근데 정식 버전으로 바뀌면서 문서가 많이 변한 모양이다.
나 공부할땐 안보였던 것도 좀 보이더라
나중에 한번 제대로 정리해서 올려보던가 해야겠다.