지금까지의 웹 페이지들은 어떤 특정 라우트 내에서 noStore
나 cookies()
와 같은 동적 함수들을 호출하면 라우트 전체가 동적으로 바뀐다.
하지만 대부분의 라우트들은 전체 페이지가 정적이거나 동적 하나만으로 결정되지 않고, 어느 부분은 정적이고 어느 부분은 동적이다. 예를들면 소셜 미디어 피드를 생각해보자. 포스트들은 정적이지만, 포스트들의 좋아요는 동적일 것이다. 전자 상거래 사이트를 생각해보면 제품 설명은 정적이지만, 유저 카트는 동적일 것이다.
Next.js 14에 새로 추가된 기능인 Partial Prerendering을 통해 페이지 내의 정적인 부분과 동적인 부분들을 분리해 렌더링할 수 있게 되었다.
만약 사용자가 특정 라우트에 방문하면,
이 방식은 라우트 전체가 정적이거나 동적인 요즈음의 웹 페이지들과는 달리 한 라우트내에 정적인 부분들을 먼저 준비주고, 동적인 부분들을 병렬적으로 처리해준다. 결국 로드 타임을 줄이고 사용자 경험을 개선할 수 있으며, Next.js 팀은 이것이 웹 어플리케이션의 디폴트 렌더링 모델이 될 잠재력이 있다고 생각하고 있다고 한다.
Partial Prerendering은 리액트의 Concurrent API들과 Suspense를 이용한다.
초기 정적파일에 fallback이 다른 정적 컨텐츠들과 함께 삽입되고, 빌드타임 또는 재평가되는 동안 라우트의 정적인 부분들은 prerendered - 사전 렌더링 된다. 나머지 부분(동적인 부분들)은 라우트의 유저 요청이 올 때까지 지연된다.
컴포넌트를 Suspense로 감싸면 컴포넌트가 동적으로 변하는게 아닌, Suspense를 라우트의 정적과 동적 경계로 활용할 수 있다는 것에 주목하자. (컴포넌트를 동적으로 바꾸려면 unstable_noStore를 사용했던 것을 떠올리자.)
Partial Prerendering의 놀라운 점은 따로 코드를 변경할 필요가 없다는 점이다. 앞서 진행했던 것처럼 라우트의 동적인 부분을 Suspense로 감싸주기만 하면 Next.js는 라우트의 어떤 부분이 정적이고 동적인지 알아서 판단할 것이다.
지금까지 데이터 페칭을 위해 진행한 최적화를 복습해보자.
useSearchParams
- 현재 URL의 파라미터들에 접근할 수 있게 해준다. 예를들어 /dashboard/invoices?page=1&query=pending
과 같은 URL의 서치 파라미터는 {page: '1', query: 'pending' }
과 같은 모습이다.usePathname
- 현재 URL의 pathname을 읽을 수 있게 해준다. 예를들어 /dashboard/invoices
라우터에서 usePathname
은 '/dashboard/invoice'
를 리턴 할 것이다.useRouter
- 라우트 간의 이동을 가능하게 해준다.유저의 검색을 구현하는 단계는 다음과 같다.
유저의 입력을 URL에 반영시키는 방법을 알아보자.
'use client';
...
export default function Search() {
const searchParams = useSearchParams();
const pathname = usePathname();
const { replace } = useRouter();
function handleSearch(term: string) {
const params = new URLSearchParams(searchParams);
if (term) {
params.set('query', term);
} else {
params.delete('query');
}
replace(`${pathname}?${params.toString()}`);
}
...
<input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
placeholder={placeholder}
onChange={(e) => {
handleSearch(e.target.value);
}}
defaultValue={searchParams.get('query')?.toString()}
/>
...
input의 onChange를 통해 유저의 입력이 들어오면 handleSearch 함수로 입력을 전달한다.
handleSearch함수에서는 useSearchParams
로 가져온 서치 파라미터들을 통해 새로운 URLSearchParams 객체를 만들어 유저의 입력을 넣어준다.
이후 usePathname
을 통해 가져온 pathname과 서치 파라미터들을 합쳐 URL을 만들고, useRouter
의 replace 함수를 통해 해당 URL로 유저를 이동시킨다.
input의 defaultValue 값을 서치 파라미터에서 가져온 값으로 지정해줌으로써 동기화시켜준다.
이렇게 유저의 입력을 서치 파라미터로 전달하고, URL을 변경시키는 방법을 알아보았다. 이제 URL에서 서치 파라미터들을 읽어와 사용하는 법을 알아보자.
...
export default async function Page({
searchParams,
}: {
searchParams?: {
query?: string;
page?: string;
};
}) {
const query = searchParams?.query || '';
const currentPage = Number(searchParams?.page) || 1;
...
<Suspense key={query + currentPage} fallback={<InvoicesTableSkeleton />}>
<Table query={query} currentPage={currentPage} />
</Suspense>
...
Page 컴포넌트들은 searchParams
라는 prop을 기본적으로 받을 수 있다. 이 prop을 통해 현재 URL의 서치 파라미터들에 접근하여 사용할 수 있다.
When to use the
useSearchParams()
hook vs thesearchParams
prop?
서치 파라미터들을 추출하는 두가지 방법이 있다는 것을 확인했다. 둘 중에 어떤 것을 사용해야 할지는 현재 서치 파라미터를 활용하는 위치가 클라이언트 단인지, 서버 단인지에 달려있다.
<Search>
컴포넌트는 클라이언트 컴포넌트였고, 클라이언트에서 서치 파라미터에 접근하기 위해서는 useSearchParams()
훅을 사용해야한다.<Table>
컴포넌트는 데이터를 페치하는 서버 컴포넌트이고, page로부터 searchParams
prop을 전달하여 사용할 수 있다.일반적으로 클라이언트에서 서치 파라미터들을 읽고 싶다면 서버로 다시 돌아가는 현상을 방지하기 위해 useSearchParams()
훅을 사용한다.
현재는 유저의 매 키보드 입력 하나하나 마다 URL을 업데이트하고 있고, 이말인 즉 매 키보드 입력마다 DB에 쿼리를 보내고 있다는 말이다. 앱이 작고 유저가 적을때는 상관없지만, 유저수가 수천명만 넘어가도 매 키보드 입력 하나하나마다 DB에 쿼리를 보내는 것은 서버에 많은 부하를 줄 것이다.
이를 방지하기 위한 기법이 Debouncing으로, 함수가 실행되는 횟수를 제한하는 기법이다.
Debouncing의 작동 원리
Debouncing을 구현하는 방법은 정말 다양하지만, 여기서는 간단히 하기 위해 use-debounce
라는 라이브러리를 사용한다.
// ...
import { useDebouncedCallback } from 'use-debounce';
// Inside the Search Component...
const handleSearch = useDebouncedCallback((term) => {
console.log(`Searching... ${term}`);
const params = new URLSearchParams(searchParams);
if (term) {
params.set('query', term);
} else {
params.delete('query');
}
replace(`${pathname}?${params.toString()}`);
}, 300);
이렇게 함수를 useDebounceCallback
이라는 훅으로 감싸주면 우리가 지정한 300ms의 타이머가 종료될 때, 즉 유저가 300ms동안 입력을 멈추고 기다릴 때만 해당 함수가 실행될 것이다.