https://taejinii.tistory.com/84
위의 글에선 Intercepting Routes를 활용하여 모달을 렌더링 하는 부분까지 소개를 해보았습니다.
이번 글에서는 쿼리 스트링으로 검색값을 활용하는 방법, Suspense를 사용하여 서버컴포넌트를 점진적으로 로딩하는 방법, error.tsx의 활용 등을 공유해 보고자 합니다.
검색 컴포넌트 구조
- (.) search : 경로를 interceptor 해주는 폴더이며 page.tsx, layout.tsx, error.tsx를 모두 사용하였습니다
- boards/chatrooms : 특정 페이지에서 검색을 하게 되면 특정 검색결과만 랜더링 해주는 페이지입니다(boards 페이지에서 검색하면 게시물만 검색됨)
- _components : private folder 로써 특정 페이지에서만 사용되는 컴포넌트들을 모아두는 곳입니다.
레이아웃 구조 잡기
// (.)search/layout.tsx
import React from 'react';
import ContentTab from './_components/ContentTab';
import SearchQueryInput from './_components/SearchQueryInput';
import SearchModal from './_components/SearchModal';
export default function SearchLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<SearchModal>
<header>
<SearchQueryInput />
</header>
<ContentTab />
{children}
</SearchModal>
);
}
search 경로를 interceptor 해주었기 때문에 layout.tsx 에서 검색컴포넌트에 공통으로 랜더링 될 클라이언트 컴포넌트들을 사용하였습니다.
이제 search 경로를 접근하게 된다면 모든 해당 페이지는 모달 형태로 페이지가 랜더링 될 것입니다.
쿼리 스트링을 활용한 검색 기능 구현
// SearchQueryInput.tsx
'use client'
import useInput from '@/hooks/common/useInput';
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
import { SearchInput } from '@/components/ui/ui';
export default function SearchQueryInput() {
const [text, onChangeText, reset] = useInput('');
const searchParams = useSearchParams();
const pathname = usePathname();
const { replace } = useRouter();
const handleSearch = () => {
const params = new URLSearchParams(searchParams);
params.set('page', '1');
if (text) {
params.set('query', text);
} else {
params.delete('query');
params.delete('page');
}
replace(`${pathname}?${params.toString()}`);
};
검색창의 value는 useInput 커스텀훅에서 관리되고 있습니다.
URLSearchParams는 URL의 쿼리 문자열을 대상으로 작업할 수 있는 유틸리티 메서드를 정의합니다.
handleSearch 함수는 다음과 같이 동작합니다.
- 함수 호출 시 params을 page=1로 세팅합니다
- 만약 text가 있다면 params에 query를 추가합니다. ex) query=검색어
- 만약 text가 없다면 page와 query를 둘 다 삭제합니다.
- pathname과 params를 이어 붙여 원하는 URL로 이동합니다. ex) 강아지를 검색했을 경우 -> search?query=강아지&page=1
replace에서 pathname을 안 붙이게 된다면 search가 URL에 제외됩니다. params는 쿼리 스트링만 관리할 뿐이기 때문입니다.
<SearchInput
onClickSearchIcon={handleSearch}
value={text}
resetValue={reset}
placeholder={handlePlaceholder()}
onChangeValue={onChangeText}
onPressEnter={() => {
handleSearch();
}}
/>
해당 함수를 디자인 시스템으로 구축된 컴포넌트에 prop으로 넘겨줍니다. handleSearch 함수는 유저가 Enter를 누를 때마다 검색을 하게 됩니다.
pathname에 따라 다양한 placeholder 적용
const handlePlaceholder = () => {
let placeholder = '채팅방 또는 게시물을 검색할 수 있어요.🐾';
if (pathname.split('/').includes('chatrooms')) {
placeholder = '채팅방을 검색하고 새로운 사람들과 소통해보세요.🐾';
}
if (pathname.split('/').includes('boards')) {
placeholder = '흥미로운 게시물을 검색할 수 있어요.🐾';
}
return placeholder;
};
pathname을 사용하면 손쉽게 동적인 placeholder를 구현할 수 있게 되어 유저에게 좋은 UX를 제공할 수 있습니다.
Suspense를 통한 서버컴포넌트 Streaming하기
Suspense를 사용하여 서버 컴포넌트를 Streaming 방식으로 가져오는 코드입니다.
import { Suspense } from 'react';
import { fetchBoardsPages } from '@/service/server/chatRoom';
import BoardList from './_components/BoardList';
import Pagination from '../_components/Pagination';
import BoardsLoading from './_components/BoardsLoading';
export default async function Page({
searchParams,
}: {
searchParams: { query: string; page: string };
}) {
const { query, page } = searchParams;
const totalPage = await fetchBoardsPages(query);
return (
<>
<section className="h-full overflow-y-auto">
<Suspense fallback={<BoardsLoading />}>
<BoardList query={query} page={page} />
</Suspense>
</section>
<Pagination totalPages={totalPage} />
</>
);
}
Suspense란?
기존 SSR 에서는 큰 단점이 하나 있었습니다. 데이터를 가져오는 동안 화면 전체가 Blocking 되어 유저는 흰 화면만 봐야 한다는 것이었습니다.
하지만 Suspense를 사용하게 된다면 데이터를 Streaming 방식을 사용하여 점진적으로 데이터를 클라이언트에 전송합니다. 즉 유저는 데이터를 가져오는 동안 흰 화면만 보는 것이 아닌 다른 컴포넌트들과 상호작용이 즉시 가능하게 됩니다.
Suspense를 사용하게 되면 여러 가지 이점이 있습니다.
- 선언적으로 코드 작성이 가능합니다. 만약 React-Query나 useEffect를 통한 로딩상태를 관리한다면 위보다 더욱 많은 양의 코드가 필요하게 될 것입니다.
- 유저는 빠른 상호작용 가능합니다. Suspense로 감싸진 컴포넌트가 데이터를 가져오는 동안 로딩 상태를 표현해 줄 수 있으며 이밖에 다른 코드들은 상호작용이 가능합니다. 즉 더 빠른 UI를 유저에게 보여줄 수 있으며 이는 유저이탈률을 줄이는 큰 역할을 할 수 있습니다.
// BoardList.tsx
import { fetchFilteredBoards } from '@/service/server/chatRoom';
import NotFoundIcon from '@/public/svgs/MagnifyingGlass.svg';
import BoardCard from './BoardCard';
export default async function BoardList({
query,
page,
}: {
query: string;
page?: string;
}) {
const boards = await fetchFilteredBoards(query, Number(page) || 1);
const isExistBoards = boards.content.length !== 0;
return (
<div className="flex flex-col h-full gap-4 mt-3">
{!isExistBoards && (
<div className="flex flex-col items-center justify-center w-full h-full">
<NotFoundIcon className="w-40 h-40" />
<p className="header2 text-grey-500">
아쉽지만, 검색 결과가 없습니다.
</p>
</div>
)}
{isExistBoards && (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{boards.content.map((board) => (
<BoardCard key={board.id} {...board} />
))}
</div>
)}
</div>
);
}
prop으로 받은 query param을 통해 서버컴포넌트에서 데이터를 요청하고 이를 랜더링 해주었습니다.
위 영상에서 보이는 것과 같이 BoardList를 제외한 검색 컴포넌트, Tab 컴포넌트, 페이지네이션 컴포넌트들은 boards 페이지 랜더링 즉시 상호작용이 가능한 것을 볼 수 있으며 BoardList컴포넌트는 로딩 중인 상태를 나타내다가 데이터를 클라이언트로 전송이 완료되면 렌더링 되는 것을 볼 수 있습니다.
error.tsx를 활용한 예기치 못한 오류 처리
error.tsx란 Next.js에서 제공하는 파일 규칙이며 이를 사용하게 되면 예기치 않은 런타임 오류를 우아하게 처리할 수 있습니다.
error.tsx 는 자식 세그먼트 또는 page.tsx를 감싸는 ReactErrorBoundary를 자동으로 생성합니다.
에러 경계 내에서 에러가 발생하면 fallback Component가 랜더링 되는데 이때 error.tsx 가 사용됩니다.
error.tsx에서는 컴포넌트 오류를 복구할 수 있는 기능도 사용할 수 있습니다.
// (.)search/error.tsx
'use client';
import { useEffect } from 'react';
import Button from '@/components/ui/Button';
import { useRouter } from 'next/navigation';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const { replace } = useRouter();
useEffect(() => {
console.error(error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center w-full h-screen gap-10">
<h2 className="header1">예기치 못한 오류가 발생하였습니다.🤯</h2>
<p>잠시후 다시 시도해주세요.</p>
<div className="flex gap-2 w-80">
<Button fullWidth onClickAction={() => replace('/')}>
메인으로
</Button>
<Button onClickAction={reset} fullWidth>
새로고침
</Button>
</div>
</div>
);
}
error.tsx가 잘 작동하는지 테스트 해보기위해 일부러 API응답을 3초간 지연시키고 에러를 발생시켜 보았습니다.
Suspense로 인해 데이터를 가져오는 3초간 Loading중인 상태가 표시되고 이후 (.)search 의 page.tsx의 에러를 catch해서 해당부분만 fallback component가 렌더링되었습니다.
나머지 layout.tsx에서 사용중인 컴포넌트들은 정상적으로 렌더링이 되며 상호작용이 가능한것을 볼수 있습니다.
구현하며 느낀 점
해당 기능들을 구현하며 Next.js의 라우팅 시스템, 파일 규칙, URL 쿼리 스트링을 관리하는 방법, Suspense와 서버컴포넌트를 사용하는 방법, 컴포넌트 에러처리 등 많은 부분들을 학습할 수 있었습니다. 특히 라우팅 시스템의 규칙에 대해 한 발짝 더욱 나아 간 거 같아 매우 매우 만족스러운 작업이었습니다.
'Next.js' 카테고리의 다른 글
Next.js Intercepting Routes 를 활용한 Modal 구현 (0) | 2023.11.12 |
---|---|
Next.js 14 App Router 가이드를 통한 Dashboard 구현 (0) | 2023.10.28 |
Nextjs - Link 태그의 Prefetch 기능 (0) | 2023.06.29 |
Next.js - Next.js 13 Metadata 동적 생성하기 (with 13.4 version) (0) | 2023.06.27 |
Next.js - Nextjs 13 정리 (1) (0) | 2023.06.20 |