client components활용 - search bar 만들기

완두콩·2023년 7월 13일
0

Next.js

목록 보기
9/16

client components -위키피디아 API로 search 기능 만들어보기!!!

Search.tsx 만들기

app폴더 안에 components 파일 만들어준다.
Search.tsx 파일을 만들고 상단에 "use client"; 를 적어 클라이언트 컴포넌트로 만들어준다.

useState와 useRouter를 임포트 해주는데 useRouter는 next/navagation에서 가져옴!.


useState를 이용해 search, setSearch를 만들어 input value를 위한 state를 만들어준다.
submit을 위한 handleSubmit을 async 함수를 이용해 만들어주는데
submit 후에 input 창을 비워주기 위해
setSearch를 ''으로 하고 router.push해서 검색어의 값으로 경로를 지정해준다.
여기서 파라미터로 들어온 e에 에러가 뜨는데 타입을 지정해주지 않았기 때문!
파라미터 e의 타입을 알기 위해서는 form에 onSubmit에 e => {}이런 형식을 적어보고 마우스를 올려보면 나온다.

react에서 FormEvent를 import해주고 onSubmit에서 확인한 타입을 적어주면 해결!

import { useState, FormEvent } from "react";

  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setSearch("");
    router.push(`/${search}/`);
  };

Search.tsx는 완성

"use client";

import { useState, FormEvent } from "react";
//useRouter가 새로운 버전 next/navigation
import { useRouter } from "next/navigation";

export default function Search() {
  const [search, setSearch] = useState("");
  const router = useRouter();

  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setSearch("");
    router.push(`/${search}/`);
  };

  return (
    <form
      onSubmit={handleSubmit}
      className="w-50 flex justify-center md:justify-between"
    >
      <input
        type="text"
        value={search}
        onChange={(e) => setSearch(e.target.value)}
        className="bg-white p-2 w-80 text-xl rounded-xl"
        placeholder="Search..."
      />
      <button className="p-2 text-xl rounded-xl bg-slate-300 ml-2 font-bold">
        🚀
      </button>
    </form>
  );
}

이제 client component로 만들어 놓은 Search를 Navbar에 넣어주기 한다. 그냥 import해서 넣어주고 싶은 위치에 넣어주면 된다!
이건 어려운거 1도 없음.

dymanic route

search에서 검색어에 따른 경로를 만들어줬으니 다이나믹 라우팅을 위한 부분을 만들어줘야한다.
다이나믹 route는 app폴더에 [] 대괄호를 이용해 폴더를 만들어주는데
[searchTerm]이라는 이름으로 폴더를 만들고 그 안에 page.tsx를 만들어
준다.

page에서 Props의 타입을 지정해주면 된다.

fetch Data

app폴더와 같은 선상에 lib 폴더를 만들어 검색 결과를 받아올 함수를 만들어준다. API를 이용해 가져오는 것이므로 당연히 async / await사용.
searchTerm의 타입은 string으로 지정해준다.

export default async function getWikiResults(searchTerm: string) {
  const searchParams = new URLSearchParams({
    action: "query", // API에게 수행할 동작을 지정. "query"는 데이터를 검색하고 추출하는 동작을 의미
    generator: "search", //검색 결과를 생성하는 동작을 지정. "search"는 검색 결과를 생성하기 위한 제너레이터(generator)를 사용하라는 의미 
    gsrsearch: searchTerm, //검색어로 사용될 변수. searchTerm 변수에 저장된 값을 사용하여 Wikipedia에서 검색할 키워드를 지정.
    gsrlimit: "20", //가져올 검색 결과의 제한 수를 설정. 이 경우 최대 20개의 결과를 가져오도록 지정
    prop: "pageimages|extracts", //반환된 페이지에 포함할 속성을 지정. "pageimages"는 페이지 이미지에 대한 정보를 가져오라는 의미이고, "extracts"는 페이지의 요약 정보를 가져오라는 의미
    exchars: "100", // 페이지 요약 정보의 최대 글자 수를 설정. 이 경우 100글자로 제한
    exintro: "true", // 페이지 요약 정보가 페이지의 소개 부분에 대한 것임
    explaintext: "true", // 페이지 요약 정보를 텍스트 형식으로 가져오라는 의미
    exlimit: "max", // 페이지 요약 정보를 최대한으로 가져오라는 의미
    format: "json", // 반환되는 데이터의 형식을 지정. 이 경우 JSON 형식으로 데이터를 반환하도록 지정.
    origin: "*", // API 호출의 출처를 나타냄. "*"는 모든 출처를 허용한다는 의미
  });
  

  const response = await fetch(
    `https://en.wikipedia.org/w/api.php?${searchParams.toString()}`
  );

  return response.json();
}

URLSearchParams를 사용하여 API 호출에 필요한 매개변수를 설정해줌(설명 달아 놓은 것은 위키피디아 API공식문서에 있음). 이 매개변수는 Wikipedia API에 전달되어 검색 결과를 필터링하고 가져오는 데 사용한다.

fetch 함수를 사용하여 Wikipedia API에 요청을 보내고 검색 결과를 가져온다. 이 URL은 searchParams.toString()을 사용하여 검색 매개변수를 문자열로 변환하고 URL에 추가. 이렇게 하면 API에 필요한 검색 매개변수가 포함된 완전한 URL이 생성된다.
응답(response)을 JSON 형식으로 변환하고 반환한다.

types.d.ts에 타입지정

가장 바깥쪽에 types.d.ts을 만들어 타입을 지정해준다.

type Result = {
  pageid: string;
  title: string;
  extract: string;
  thumbnail?: { //썸네일은 없을수도 있으므로 옵셔널(?)표시해줌
    source: string;
    width: number;
    height: number;
  };
};

//검색할 때의 타입 - 검색결과가 없을수도 있으므로 쿼리와 페이지에 옵셔널로 해줌.
type SearchResult = {
    query?: {
        pages?: Result[],
    }
}

검색 결과 보여주기

다시 [searchTerm]폴더의 page.tsx로 가서 lib폴더에 만들어놓은 검색어를 통해 위키 데이터를 가져오는 함수를 불러와주고 그 결과 SearchResult으로 해서 데이터를 가져온다.


const results: Result[] | undefined = data?.query?.pages;

data 객체의 query 속성의 pages 속성을 results 변수에 할당.
이 속성은 API 호출로부터 반환된 검색 결과 페이지의 배열이다.
Result[] 타입으로 선언되었으며, undefined일 수도 있다- 검색 결과가 없을 시..
data?.query?.pages의 형태로 옵셔널 체이닝을 사용하여 객체의 속성에 접근하고, 존재하지 않는 경우에는 undefined를 반환한다.

const content = {results ? (
       Object.values(results).map((val) => {
         return <Item key={val.pageid} result={val} />;
       })
     ) : (
       <h2>{`${searchTerm}에 대한 결과가 없습니다.`}</h2>
     )}
     
return content

내용은 조건문을 써서 결과값이 존재할 때와 그렇지 않을 때로 나누어서 반환하는데
객체의 값들을 배열로 반환하는 Object.values(results)을 써서 result를 배열로 바꿔주고 배열의 각각을 val로 Item이라는 컴포넌트를 만들어서 보여준다.

Item 컴포넌트

[searchTerm]폴더에 components폴더를 만들어 Item.tsx 파일을 만들어준다.

<Item key={val.pageid} result={val} />

result라는 이름으로 값을 보냈기 때문에
Item.tsx에서 Props의 타입을 types.d.ts에서 정해놓은 Result타입으로 받아와준다.

import Link from "next/link";

type Props = {
  result: Result;
};

export default function Item({ result }: Props) { 

 const itemTextCol = (
    <div className="flex flex-col justify-center">
      <h2>
        <Link
          href={`https://en.wikipedia.org/?curid=${result?.pageid}`}
          target="_blank"
        >
          {result?.title}
        </Link>
      </h2>
      <p>{result?.extract}</p>
    </div>
  );

  const content = result?.thumbnail?.source ? (
    <article className="m-4 max-w-lg">
      <div className="flex flex-row gap-4">
        <div className="flex flex-col justify-center">
          <img
            src={result?.thumbnail?.source}
            alt={result?.title}
            width={result?.thumbnail?.width}
            height={result?.thumbnail?.height}
            loading="lazy"
          />
        </div>
        {itemTextCol}
      </div>
    </article>
  ) : (
    <article className="m-4 max-w-lg">{itemTextCol}</article>
  );

  return content;

}

itemTextCol은 하나의 검색어로 받아와지는 수많은 결과물들에 대한 상세 페이지로 이동하기 위한 것이다.
각 결과물의 title과 요약extract을 나타내고
title을 누르면 Link에 의해 pageID로 결과의 상세 페이지로 이동한다.

content에는 만약 섬네일이 있다면 섬네일과 itemTextCol내용을 반환하고 그렇지 않다면 itemTextCol만 반환하여 결과적으로 사용자에게 보여주게 된다.

검색어에 따른 metadata

[searchTerm]/page.tsx로 가서 검색어에 따라 헤드 타이틀이 변하는 다이나믹 메타데이터를 만들어준다.

export async function generateMetadata({ params: { searchTerm } }: Props) {
  const wikiData: Promise<SearchResult> = getWikiRes(searchTerm);
  const data = await wikiData;
  const displayTerm = searchTerm.replaceAll("%20", " ");

  if (!data) {
    return {
      title: `${displayTerm}을 찾을 수 없습니다.`,
    };
  }

  return {
    title: displayTerm,
    description: `${displayTerm}에 대한 결과입니다.`
  }
}

const displayTerm = searchTerm.replaceAll("%20", " ");
URL에서는 공백을 직접 사용할 수 없다. 대신 %20이라는 인코딩된 형식을 사용.
예를 들어, "hello world"라는 문자열은 URL에서는 "hello%20world"로 표현된다. 그래서 검색어에 공백이 들어가있을 시에는 그것을 head에 title로 나타내줄 경우 %20문자열을 공백으로 대체해서 나타내준다는 의미.

결과

jen을 검색했을 떄 상단 타이틀도 잘 바뀌었고 url경로도 검색어에 따라 잘 바뀌었다. 그리고 그에 따른 데이터도 Item.tsx에서 만들어준대로 섬네일이 있으면 함께 나오고 아니면 타이틀과 요약만 나타내준다.

타이틀을 누르면 itemTextCol에서 링크 걸어놓은 대로 위키피디아에 페이지 아이디로 연결되어 결과의 상세 페이지로 연결된다.

profile
공부하자. 기록하자. 쫌!

0개의 댓글