본문 바로가기

개발/Next

[Next14] 검색 기능 만들기 with Supabase

반응형

기능 정의

  • 검색창에 검색어를 입력하면 `title` 또는 `location`에 검색어가 포함된 데이터를 노출시킨다.
  • 뒤로가기를 눌렀을 때, 이전 검색 결과로 이동한다.

컴포넌트 구현

검색창 코드

"뒤로가기를 눌렀을 때, 이전 검색 결과로 이동한다."라는 기능을 구현하기 위해서,

검색어를 입력했을 때 히스토리 스택이 추가되어야 합니다.

따라서 검색어를 검색하면 url 쿼리를 추가하여 히스토리 스택을 쌓아봅시다.

 

import { Input } from "@nextui-org/input";
import { ChangeEvent, useState } from "react";

interface SearchInputProps {
  onSubmit: (value: string) => void;
}
const SearchInput = ({ onSubmit }: SearchInputProps) => {
  const [value, setValue] = useState("");
  
  return (
    <form
 onSubmit={(e) => 
      {
        e.preventDefault();
        onSubmit(value);
      }}
    >
      <Input
        value={value}
        onChange={(e) =>
        	{ setValue(e.target.value) }
        }
        placeholder="테마명/지점 검색"
        autoComplete="off"
        enterKeyHint="search"
      />
    </form>
  );
};

export default SearchInput;

`form`이 제출되었을 때 화면이 깜빡이는 현상을 막기 위해 `e.preventDefault()`를 실행시킨 후,

props로 받은 `onSubmit`함수에 검색어를 파라미터에 넣고 호출합니다.

 

<SearchInput onSubmit={(search) => router.push(`?search=${search}`)} />

`SearchInput`의 부모 컴포넌트에서 위와 같이 `prop`을 넘겨줍니다.

`onSubmit` 함수의 파라미터로 전달받은 값을 url 쿼리에 포함시켜 `router.push`합니다.

 

 

검색 페이지 코드

import { useRouter, useSearchParams } from "next/navigation";

const searchParmas = useSearchParams();
const search = searchParmas.get("search");

url이 `https://url.com?search=검색어` 일 경우,

`search`에 해당하는 "검색어" 라는 문자열을 가져오기 위해서 위와 같은 코드를 사용할 수 있습니다.

 

import { useRouter, useSearchParams } from "next/navigation";

// ...

const searchParmas = useSearchParams();
const search = searchParmas.get("search");

useEffect(() => {
    const fetchThemes = async () => {
      setIsLoading(true);
      let url = `api/theme`;
      if (search) url += `?search=${search}`;

      try {
        const response = await fetch(url);
        const { data } = await response.json();
        setThemes(data);
      } finally {
        setIsLoading(false);
      }
    };
}, [search])

url에서 가져온 `search` 값을 사용하여 next API 를 호출하는 코드입니다.

url의 `searchParam`에 `search` 값이 있다면 API url에 `? search=${search}`라는 쿼리를 추가합니다.

`fetch`를 사용하여 API를 호출하고 받아온 `data`를 `themes` state에 저장합니다.

 

url의 `search` 값이 변경되면 API를 다시 호출하기 위해 `dependency`에 `search`를 추가합니다.

 

{themes.map((theme) => (
  <ThemeCard key={theme.id} theme={theme} />
))}

`themes` state에 저장한 값을 다음과 같이 노출시키면 완성입니다!

 

"use client";

import { useRouter, useSearchParams } from "next/navigation";
// ...
import SearchInput from "~/app/themes/components/SearchInput";

const SearchPage = () => {
  const router = useRouter();
  const searchParmas = useSearchParams();
  const search = searchParmas.get("search");

  const [themes, setThemes] = useState<Theme[]>([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const fetchThemes = async () => {
      setIsLoading(true);
      let url = `api/theme`;
      if (search) url += `?search=${search}`;

      try {
        const response = await fetch(url);
        const { data } = await response.json();
        setThemes(data);
      } finally {
        setIsLoading(false);
      }
    };

    fetchThemes();
  }, [search]);
  
  if (isLoading) return <></>

  return (
    <>
      <SearchInput onSubmit={(search) => router.push(`?search=${search}`)} />
      <Spacing px={30} />
      <div className="flex pl-[12px]">
        {search ? (
          <Text>&quot;{search}&quot; 검색 결과</Text>
        ) : (
          <Text>전체 검색 결과</Text>
        )}
      </div>
      <Spacing px={16} />
      <ul className="grid grid-cols-2 gap-[8px]">
        {themes.map((theme) => (
          <ThemeCard key={theme.id} theme={theme} />
        ))}
      </ul>
    </>
  );
};

export default SearchPage;

전체 코드는 위와 같습니다.

 

API 구현

위에서 구현한 `fetchThemes` 함수를 보면 api 엔드포인트는 `api/theme` 입니다.

따라서 `src/app/api/theme/route.ts` 에 있는 `GET` 함수를 만들어봅시다.

// src/app/api/theme/route.ts
import { NextRequest, NextResponse } from "next/server"

export function GET(req: NextRequest) {
  const search = req.nextUrl.searchParams.get('search')
  
  return NextResponse.json({})
}

검색하고자 하는 검색어를  엔드포인트에 `?search=${검색어}`와 같이 쿼리를 붙여주었습니다.

`req.nextUrl.searchParams.get('search')` 를 실행하여 검색어의 값을 가져올 수 있습니다.

 

const data = await supabase
	.from('theme')
	.select('*')
	.like('title', `%${search}%`)

supabase의 데이터 중 `title`에 검색어를 포함한 데이터를 찾기 위해서 `like` 함수를 사용할 수 있습니다.

 

const data = await supabase
	.from('theme')
	.select('*')
	.or(`title.like.%${search}%,location.like.%${search}%`)

 

하지만 기능 정의를 살펴보면 `title` 뿐만 아니라, `location` 컬럼에도 검색어가 포함되어 있는지 함께 검색해야 합니다.

이럴 땐 여러 쿼리를 사용할 수 있는 `or` 함수를 사용합니다.

 

export async function GET(req: NextRequest) {
  const supabase = createClient();

  let query = supabase.from("theme").select("*");

  const search = req.nextUrl.searchParams.get("search");
  if (search) {
    query = query.or(`title.like.%${search}%,location.like.%${search}%`);
  }

  const { data, error, status } = await query;

  return NextResponse.json({ data, error }, { status });
}

전체 코드입니다.

`search`의 유무에 따라서 `like` 쿼리 실행 여부를 결정했습니다.

supabase 쿼리를 실행한 결과는 `NextResponse`에 `json` 형태로 담아 전달해 줍니다.


결과

검색창에 "포레"라고 검색했을 때, `url`에 `search="포레"` 쿼리가 추가되고,

결과값으로 "포레"를 포함하고 있는 항목이 성공적으로 노출됩니다. 🎉

 

반응형