Next.js, React, TypeScript를 사용하여 서버와 통신하여 검색창 필터 기능을 구현하는 방법을 알아보자!
만들 예제의 완성본
검색창에 키워드를 검색하면, 해당 검색 결과와 일치하는 데이터를 서버에서 가져와서 보여준다.
구현 코드
index.tsx src/pages/search/index.tsx
(검색 리스트를 보여주는 페이지)
import SearchableLayout from "@/components/searchable-layout";
import { ReactNode } from 'react';
import BookItem from "@/components/book-item";
import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
import fetchBooks from "@/lib/fetch-books";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const q = context.query.q;
const books = await fetchBooks(q as string);
return {
props: { books, },
};
};
export default function Page({ books }: InferGetServerSidePropsType<typeof getServerSideProps>) {
return <div>
{books.map((book) => (
<BookItem key={book.id} data={book} />
))}
</div>
}
Page.getLayout = (page: ReactNode) => {
return <SearchableLayout>{page}</SearchableLayout>
}
SearchableLayout.tsx src/components/searchable-layout.tsx
(검색창 레이아웃 컴포넌트)
import { useRouter } from "next/router";
import { ReactNode, useEffect, useState } from "react";
import style from "./searchable-layout.module.css";
export default function SearchableLayout(
{ children, }: { children: ReactNode }
) {
const router = useRouter();
const q = router.query.q as string;
const [search, setSearch] = useState("");
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>
<div className={style.searchbar_container}>
<input
value={search}
onKeyDown={onkeyDown}
onChange={onChangeSearch}
placeholder="검색어를 입력하세요 ..." />
<button onClick={onSubmit}>검색</button>
</div>
{children}
</div>
)
}
fetch-books.ts src/lib/fetch-books.ts
(서버 통신으로 데이터 가져오는 함수)
import { BookData } from "@/types";
export default async function fetchBooks(q?:string):Promise<BookData[]> {
let url = `서버 URL 입력`
if(q){
url += `/search?q=${q}`
}
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error();
}
return await response.json();
} catch (err) {
console.error(err);
return [];
}
}
코드 설명
1. 검색창 레이아웃 컴포넌트 (SearchableLayout)를 생성한다.
1) Next.js의 useRouter로 URL 쿼리 가져오기
const router = useRouter();
const q = router.query.q as string;
useRouter 훅을 사용하여 URL에 포함된 q 쿼리값을 가져와 search 상태로 설정한다.
2) useEffect로 초기화
useEffect(() => {
setSearch(q || "");
}, [q]);
검색어가 변경될 때마다 search 상태가 업데이트된다.
* 여기서 useEffect를 사용하는 이유:
Next.js에서는 페이지가 처음에 서버에서 렌더링되기 때문에 useEffect를 사용하여 클라이언트에서만 실행되도록 코드를 작성하는 경우가 많다.
서버 사이드 렌더링(SSR) 단계에서는 window, document와 같은 브라우저 관련 객체에 접근할 수 없기 때문에,
클라이언트 측에서만 필요한 작업은 useEffect에 넣어 클라이언트에서만 실행되게 한다. (useEffect의 기본 동작이 컴포넌트가 처음 마운트될 때(즉, 브라우저에 렌더링된 후)에만 실행되기 때문에)
예를 들어, router에서 query 파라미터를 읽어오는 작업은 브라우저에서만 가능한데, useEffect를 통해 클라이언트 사이드에서만 query 값을 읽고 상태를 설정하도록 할 수 있다.
이렇게 하면 서버에서 렌더링된 페이지가 클라이언트로 전달되고 나서 브라우저에서만 해당 작업이 실행되기 때문에, SSR과 CSR 간의 차이를 효과적으로 처리할 수 있게 된다.
3) 마크업을 통해 검색창을 만들고, 함수 달아주기
<div>
<div className={style.searchbar_container}>
<input
value={search}
onKeyDown={onkeyDown}
onChange={onChangeSearch}
placeholder="검색어를 입력하세요 ..." />
<button onClick={onSubmit}>검색</button>
</div>
{children}
</div>
<input /> 필드와 <button>을 추가하여 사용자가 검색어를 입력하고 검색할 수 있도록 한다.
- value 값에는 `const [search, setSearch] = useState("");` 에서 상태에 해당하는 search를 넣는다.
- onKeyDown은 사용자가 검색어를 입력하고 Enter를 치면 Submit(검색)을 할 수 있게 하는 함수이다.
- onChangeSearch 함수를 통해 `setSearch(e.target.value)`가 되어서, 검색어를 입력할 때마다 상태가 업데이트된다.
- onSubmit 함수로 검색어 제출 처리한다.
onSubmit 함수
const onSubmit = () => {
if (!search || q === search) return;
router.push(`/search?q=${search}`);
};
사용자가 검색어를 입력하면 URL에 q 파라미터로 검색어를 포함하여 페이지를 리로드하지 않고도 검색 결과를 표시할 수 있게 한다.
2. 검색어에 따라 데이터를 가져오는 FetchBooks함수를 만든다.
let url = `http://localhost:12345/book`;
if (q) {
url += `/search?q=${q}`;
}
검색어(q)가 있는 경우 URL에 쿼리 파라미터를 추가하여 검색어에 맞는 데이터를 가져온다.
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error();
}
return await response.json();
} catch (err) {
console.error(err);
return [];
}
}
- fetch 함수를 사용하여 서버에 데이터를 요청하고, 성공 시 JSON으로 변환하여 반환한다.
- try...catch문을 사용하여 요청이 실패하는 경우 콘솔에 에러를 출력하고 빈 배열을 반환하여 앱이 중단되지 않도록 처리한다.
3. 검색 리스트를 보여주는 페이지 컴포넌트를 구성한다.
1) 페이지 파일 생성 및 검색 레이아웃 설정
Page라는 파일을 생성하고, Page.getLayout 함수를 통해 SearchableLayout을 페이지 레이아웃으로 사용하게 설정한다.
Page.getLayout = (page: ReactNode) => {
return <SearchableLayout>{page}</SearchableLayout>
}
2) getServerSideProps 함수를 만들고, 아까 만든 fetchBooks 함수를 사용해서, 검색어에 따른 데이터를 서버에서 받아와 books라는 props로 전달한다.
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const q = context.query.q;
const books = await fetchBooks(q as string);
return { props: { books } };
};
context, return값에 대한 자세한 설명은 맨 하단에 서술하겠다.
3) 검색결과를 리스트로 렌더링
export default function Page({ books }: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<div>
{books.map((book) => (
<BookItem key={book.id} data={book} />
))}
</div>
);
}
검색 결과를 books 배열로 받아 각 책 정보를 화면에 표시한다.
코드 설명 QnA (feat. ChatGPT)
1. 아래 코드에서 getServerSideProps, context, query, {props:{books}}는 무슨 뜻인가요?
// index.tsx
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const q = context.query.q;
const books = await fetchBooks(q as string);
return { props: { books } };
};
getServerSideProps는 Next.js에서 서버 사이드 렌더링(SSR)을 위해 제공하는 함수로, 페이지가 요청될 때마다 서버에서 데이터를 가져와 클라이언트에 전달할 수 있게 해줍니다. 이 함수의 매개변수인 context와 props 반환값에 대해 하나씩 설명해 드릴게요.
1) context
context는 getServerSideProps 함수에 기본적으로 전달되는 객체입니다. 이 객체에는 요청과 관련된 다양한 정보가 포함되어 있어, 요청에 맞춘 데이터를 가져오는 데 활용할 수 있습니다. 주요 속성들은 다음과 같습니다.
- query: URL에 포함된 쿼리 파라미터를 객체 형태로 제공합니다. 여기서 context.query.q는 q라는 이름의 쿼리 파라미터 값을 가져옵니다. 예를 들어 /page?q=searchTerm와 같이 요청하면 context.query.q의 값은 searchTerm이 됩니다.
- params: 동적 라우팅의 매개변수 정보를 담고 있습니다.
- req와 res: HTTP 요청과 응답 객체로, 특정 서버 로직이 필요할 때 사용할 수 있습니다.
이 외에도 다양한 정보를 담고 있어, 각 페이지 요청에 필요한 정보를 서버에서 처리할 수 있도록 돕습니다.
2) { props: { books } }의 의미
이 부분은 getServerSideProps 함수가 반환하는 객체입니다. Next.js는 이 반환값을 받아 페이지 컴포넌트에 props로 전달합니다. { props: { books } }는 다음과 같은 의미를 가집니다:
- props 키: getServerSideProps에서 반환한 데이터는 페이지 컴포넌트로 props로 전달됩니다. 즉, props라는 이름으로 데이터를 전달하겠다는 의미입니다.
- { books }: fetchBooks 함수로 받아온 데이터를 books라는 이름의 키로 감싸 props로 전달하겠다는 뜻입니다. 이렇게 전달된 props.books는 해당 페이지 컴포넌트에서 사용할 수 있습니다.
1-1. return { props: { books } }; 이렇게 props라는 이름으로 데이터를 전달 했는데,
왜 export default function Page({ books } 이런식으로 {books}라는 이름으로 받는건가요?
Next.js에서는 getServerSideProps 함수에서 반환하는 객체의 구조를 자동으로 해석하여 해당 페이지 컴포넌트에 props로 전달합니다.
여기서 { props: { books } } 부분은 Next.js에 의해 다음과 같이 처리됩니다.
1) getServerSideProps 함수가 { props: { books } }라는 객체를 반환하면, Next.js는 props 내부에 있는 데이터를 페이지 컴포넌트로 전달할 준비를 합니다.
2) 이때 props라는 이름의 키 안에 있는 { books } 객체가 페이지 컴포넌트에 자동으로 전파되므로, 결국 Page 컴포넌트는 { books }라는 props를 직접 받을 수 있게 됩니다.
따라서 페이지 컴포넌트 Page에서 ({ books })로 직접 받는 것은 Next.js의 구조 분해 할당을 활용한 자동 props 전달 방식 덕분입니다.
구조를 예시로 표현해 보면
// getServerSideProps의 반환값 예시:
{
props: { books: [ /* books 배열 */ ] }
}
// 컴포넌트에 전달되는 props는 결국
props = { books: [ /* books 배열 */ ] } 이므로
Page({ books }) 로 바로 받을 수 있는 것
이 덕분에 Page 컴포넌트는 props.books 대신 { books }로 바로 접근하게 되는 것이죠.
1-2. 그럼 {books}라고 써도, props.books라고 자동으로 인식된다는건가요?
맞아요! { books }라고 쓰면 props.books와 동일하게 자동으로 인식됩니다.
Next.js는 getServerSideProps 함수에서 반환된 { props: { books } } 객체의 props 내부 데이터를 자동으로 페이지 컴포넌트에 전달합니다. 그래서 Page 컴포넌트는 props.books로 접근할 필요 없이 바로 ({ books })처럼 구조 분해 할당을 사용해 받아올 수 있습니다.
이를 통해 Next.js는 props 객체를 전달하는 과정을 간소화하고, 필요한 데이터만 바로 사용할 수 있게 해줍니다.
2 . 검색창 컴포넌트에서 const q = router.query.q 이런식으로 쿼리 파라미터 값을 가져온거를 쓰면 되는데 왜 굳이 context.query.q 이렇게 한번 더 가져오는건가요?
좋은 질문입니다! router.query.q와 context.query.q는 쿼리 파라미터를 가져오는 역할은 같지만, 각각 동작하는 시점과 목적이 다릅니다.
1) router.query.q vs context.query.q 차이
- router.query.q는 클라이언트 측에서 Next.js의 useRouter 훅을 사용해 쿼리 파라미터를 가져옵니다. 이 방법은 컴포넌트가 클라이언트에서 마운트된 후에만 사용할 수 있습니다. 즉, 페이지가 로드되고 브라우저에서 실행될 때 router.query.q를 통해 쿼리 파라미터를 읽습니다.
- context.query.q는 서버 측에서 getServerSideProps 함수가 실행될 때, 페이지가 요청될 때마다 쿼리 파라미터를 바로 가져옵니다. 이 방법은 서버 측 렌더링 시점을 이용해서 브라우저가 페이지를 요청하는 즉시 쿼리 파라미터에 접근할 수 있게 합니다. Next.js는 이 데이터를 서버에서 미리 가져와 페이지에 전달하고, 페이지가 클라이언트로 렌더링될 때 props로 바로 사용할 수 있게 해줍니다.
2) context.query.q를 사용하는 이유
Next.js에서 SSR을 사용할 때, 서버에서 데이터를 미리 가져와 렌더링하려면 클라이언트 측의 router.query.q 대신 getServerSideProps 함수 안의 context.query.q를 이용해야 합니다. 이렇게 하면,
- 페이지가 서버에서 바로 필요한 데이터를 미리 가져오고,
- 이 데이터를 props로 전달해 페이지 컴포넌트에 사용할 수 있게 해줍니다.
3) 왜 둘 다 필요한가?
이 코드에서는 getServerSideProps를 통해 서버에서 쿼리 파라미터를 기반으로 데이터를 가져오고, 컴포넌트에서는 useRouter를 통해 클라이언트 측에서 검색어 상태를 관리하고 라우팅을 처리합니다. 각각의 목적과 실행 시점이 다르므로 router.query.q와 context.query.q를 둘 다 사용하는 경우가 있습니다.
2-1 . 그럼 router.query.q를 통해서, 검색어 상태를 관리하는데만 쓰이고, fetchBooks 함수에서 url로 데이터 요청할때 전달하는 q는 context.query.q를 사용하는건가요?
네, 정확합니다!
이 코드에서는 각각의 역할에 맞춰 router.query.q와 context.query.q를 구분해서 사용하고 있습니다.
1) 검색어 상태 관리는 클라이언트 측에서 이루어져야 하므로, router.query.q를 사용하여 검색어 상태(search)를 관리합니다. 이 방식은 브라우저에서 사용자 입력이 실시간으로 반영되는 UI를 관리하기에 적합합니다.
2) 데이터 요청에 필요한 쿼리 파라미터는 서버 측에서 getServerSideProps 함수 내의 context.query.q를 통해 가져옵니다. 이 값을 fetchBooks 함수에 전달하여 검색어에 맞는 데이터를 서버에서 먼저 가져오고, 그 데이터를 페이지 컴포넌트의 props로 내려보내어 SSR로 초기 검색 결과를 렌더링합니다.
* 요약
- router.query.q: 클라이언트 측 상태 관리와 라우팅을 위해 사용.
- context.query.q: 서버 측 데이터 요청을 위해 사용.
이렇게 나누어 사용하면, 서버 측에서 초기 검색 결과를 미리 받아와 렌더링하고, 클라이언트 측에서는 사용자가 검색어를 변경할 때마다 실시간으로 UI가 업데이트됩니다.
'Frameworks > Next.js' 카테고리의 다른 글
[Next.js] 라우팅 (Routing) (App Router ver.) (0) | 2024.11.14 |
---|---|
[Next.js] Next.js 프로젝트 시작하기 (App Router ver.) (6) | 2024.11.14 |
[Next.js] 프리페칭(Pre-fetching) (2) | 2024.11.07 |
[Next.js] 네비게이팅(Navigating) (0) | 2024.11.07 |
[Next.js] 라우팅 (Routing) (Page Router ver.) (2) | 2024.11.07 |