React의 서버 컴포넌트
는 React 18버전부터 새롭게 추가된 유형의 컴포넌트로, 기존 클라이언트 컴포넌트와 달리 서버 측에서만 실행된다. 이는 기존의 Page Router와 다르게 브라우저에서 실행되지 않아 JS 번들에 포함되지 않으므로 브라우저 전달과 하이드레이션(hydration) 성능을 최적화하는데 중요한 역할을 한다.
기존의 Page Router
방식에서는 사전 렌더링
이라는 과정을 거쳐서 사용자가 요청한 페이지를 화면에 렌더링 하고, 이때 사전 렌더링 과정 중에 화면에 상호작용(Hydration)을 추가하기 위해서 작성한 모든 컴포넌트들을 JS번들로 묶어서 브라우저에게 후속으로 전달해주어 다시 실행한다. 하지만 Next.js에서 생성하는 모든 컴포넌트가 상호작용을 필요로 하는 것은 아니다. 즉, 이벤트 핸들러
나 useState
, useEffect
와 같은 React Hook
이 없는 단순한 정적 UI는 굳이 번들에 포함되지 않아도 된다.
그러나 기존의 Page Router
방식에서는 모든 컴포넌트를 묶어 브라우저로 전송하므로 불필요하게 번들이 커지고, 로딩 및 하이드레이션 시간이 늘어나 TTI(time to interactive)
가 길어지는 문제점이 있었다.
이 문제를 해결하기 위해 Next.js의 App Router
는 서버 컴포넌트라는 새로운 개념을 도입했다. 서버 컴포넌트는 상호작용이 필요하지 않은 컴포넌트로서, 서버에서만 실행된다. 이로 인해 컴포넌트의 일부만 브라우저에 전달하게 되어 JS 번들의 크기가 줄어들고, 하이드레이션 속도가 빨라져 페이지 성능이 개선된다.
App Router
버전에서는 컴포넌트가 상호작용이 필요한지 여부에 따라 서버 컴포넌트와 클라이언트 컴포넌트로 나뉜다.
서버 컴포넌트: 브라우저에 전달되지 않고, 서버에서 사전 렌더링 시 한 번만 실행된다. React Hook
과 같은 상호작용 기능이 없으므로 번들 크기가 줄어들어 성능에 유리하다.
클라이언트 컴포넌트: 상호작용이 필요한 경우, "use client"
라는 지시자를 통해 설정한다. 브라우저와 서버 양측에서 실행되며, Hydration
을 위해 번들에 포함된다.
서버 컴포넌트와 클라이언트 컴포넌트를 구분하는 기준은 간단하다
상호작용이 필요하다면 클라이언트 컴포넌트로 지정한다.
상호작용이 필요 없다면 서버 컴포넌트로 둔다.
이렇게 컴포넌트를 분류하면, Next서버가 브라우저로부터 접속 요청을 받아서 사전 렌더링을 진행하는 이 과정에서는 일단 HTML페이지를 한 번 생성을 해야되기 때문에 서버 컴포넌트던 클라이언트 컴포넌트던 모두 다 동일하게 한 번은 실행이 된다.
그 이후에 hydration을 위해서 컴포넌트들을 모아 JS번들로 전달하는 이 과정에서는 서버 컴포넌트
들은 제외된다.
그래서 클라이언트 컴포넌트만 JS번들에 포함이 되어서 브라우저에게 전달이 되기 때문에 결국 클라이언트 컴포넌트들만 브라우저측 즉 클라이언트 측에서 한 번 더 따로 실행이 된다.
서버 컴포넌트
는 우리가 따로 만들 필요가 없다. 왜냐면, App Router
에서는 기본적으로 모든 컴포넌트가 다 서버 컴포넌트
로 작동하기 때문이다. App Router
에 존재하는 모든 컴포넌트들은 default로 다 서버 컴포넌트로 설정이 되기 때문에 클라이언트인 브라우저 측에서는 실행되지 않는다.
그렇기 때문에, 서버 컴포넌트에서는 보안이 중요한 작업, 서버에서 데이터를 불러오는 작업을 수행할 수 있다. 예를 들어, 도서 목록이나 페이지 헤더와 같은 정적 콘텐츠를 서버 컴포넌트로 설정할 수 있다. 서버 컴포넌트는 기본적으로 서버에서만 실행되기 때문에, 브라우저 측에서는 보이지 않도록 비밀 키와 같은 보안 데이터를 컴포넌트 내부에서 안전하게 사용할 수 있다.
컴포넌트 안에서 직접 데이터를 불러오도록 설정하는 것도 가능하다. 그러니까 이전에 PageRouter 버전에서 serverSideProps
나 getStaticProps
가 했었던 역할을 이 컴포넌트가 그대로 할 수 있게 설정까지 해줄 수 있다.
// 서버 컴포넌트 예시
export default function BookList() {
const books = fetchBooksFromServer();
return (
<div>
{books.map(book => (
<div key={book.id}>{book.title}</div>
))}
</div>
);
}
그리고 도서 아이템도 서버 컴포넌트로 만들어 줄 수 있다. 왜냐면 여기에는 아무런 상호작용도 없기 때문이다. 하지만 아이템의 경우는 클릭하면 상세 페이지로 이동하니까 상호작용을 한다고 생각할 수 있는데 link는 html의 고유 기능이기 때문에 JS의 기능을 활용하는 상호작용은 아니어서 이런 경우에는 해당되지 않는다.
SearchBar
와 같이 상호작용이 필요한 컴포넌트는 "use client"
지시어를 통해 클라이언트 컴포넌트로 지정할 수 있다. 예를 들어, input 요소에서 input값을 state에 보관하고 있고 게다가 엔터를 누르면 onKeyDown
핸들러가 작동해서 페이지를 programmatic하게 이동시켜주기 때문에 이렇게 사용자의 입력을 실시간으로 반영하는 searchbar같은 컴포넌트는 클라이언트 컴포넌트로 설정해서 hydration될 수 있게 만들어 줘야 한다.
클라이언트 컴포넌트의 양을 줄여야 한다 했기 때문에 레이아웃 전체를 클라이언트 컴포넌트로 만들기 보다는 searchbar만 따로 클라이언트 컴포넌트로 만들어서 배치를 시켜주게 된다.
"use client";
import { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import style from "./serachbar.module.css";
export default function Searchbar() {
const router = useRouter();
const searchParams = useSearchParams();
const [search, setSearch] = useState("");
const q = searchParams.get("q");
useEffect(() => {
setSearch(q || "");
}, [q]);
const onChangeSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
};
const onSubmit = () => {
if (!search || q === search) return;
router.push(`/search?q=${search}`);
};
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
onSubmit();
}
};
return (
<div className={style.container}>
<input
value={search}
onChange={onChangeSearch}
onKeyDown={onKeyDown}
/>
<button onClick={onSubmit}>검색</button>
</div>
);
}
App Router에서는 app 폴더 내에서 컴포넌트와 페이지를 함께 구성할 수 있다. 파일 이름이 page나 layout이 아니라면 그냥 일반적인 자바스크립트 또는 일반적인 타입스크립트 파일의 컴포넌트로 간주되므로, 각 페이지와 관련된 컴포넌트를 같은 폴더에 둘 수 있다. 이를 Co-location
이라 부르며, 이런 점을 잘 활용하면 페이지마다 필요한 컴포넌트들을 page파일과 함께 모아둘 수 있다는 장점이 있다. 페이지와 컴포넌트들을 모아 구성하기 편리한 방식이다.
App Router를 활용한 서버 컴포넌트와 클라이언트 컴포넌트는 페이지 성능 최적화에 중요한 역할을 한다. 기본적으로 컴포넌트는 서버 컴포넌트로 실행되고, 필요한 경우에만 클라이언트 컴포넌트로 설정하여 JS 번들 크기와 하이드레이션 속도를 최적화할 수 있다.
그렇기 때문에 Next.js의 공식 문서에서는 페이지의 대부분을 서버 컴포넌트
로 구성할 것을 권장하고 클라이언트 컴포넌트
는 꼭 필요한 경우에만 사용할 것을 권장한다. 왜냐면 결과적으로 페이지 내부에 클라이언트 컴포넌트의 개수가 줄어들수록 Next서버가 브라우저에게 전달하게 되는 JS번들의 용량도 함께 줄어들기 때문이다.