React와 Node.js에서 네이버 책 검색 API 활용하기(feat.axios)

Sujeong K·2023년 1월 28일
2


독서 기록 서비스(Book극곰)를 만들 때 제일 구현 하고싶었던 기능인 책 검색 API! 이를 만들면서 겪은 시행착오와 어떻게 구현했는지 기록✨

네이버 검색 API를 이용하려면 네이버 개발자 센터에서 먼저 내 애플리케이션을 등록해야한다.
네이버 책 검색 API 설명서

카카오와 알라딘에서도 도서 검색 OPEN API를 제공하지만 나는 기존에 등록했던 애플리케이션이 있었기 때문에 네이버 책 검색 API를 사용하기로 했다.

설명서의 방법대로 요청 url과 header를 세팅하고 포스트맨에서 성공적으로 응답을 받은 것 까지는 좋았는데..

브라우저에서 맞닥뜨린 CORS 에러😭(예상은 했지만..)

그래서 https://cors.sh/ 라는 proxy를 거쳐서 요청을 보내니까 이번에는 request header 혹은 cookie가 너무 크다는 에러가 떴다.

쿠키를 지워보고, 헤더 설정을 바꿔보고 해봐도 해결은 안되고 시간이 너무 오래 걸려서 카카오나 다른 도서 검색 API를 사용해야 하나, 다른 proxy middleware를 설치해야하나 고민하다가 '어쨌든 포스트맨에서는 문제없이 동작하니까 백엔드 쪽에서 imageRouter를 만들어서 요청을 보내는 코드를 짜보면 어떨까!🤨' 하는 (무모한) 생각이 들었고 한 번 해보자 싶어서 back 디렉토리에서 작업을 했다.

네이버 검색 API 예제에서 Node.js 코드는 request 객체를 이용하는 걸로 나와있었는데 나는 axios로 요청을 보냈다. (var 키워드도 고치고..)

// imageRouter.js
import { Router } from "express";
import axios from "axios";
import { config } from "../config";

export const imageRouter = Router();

function imageSearch(req, res, next) {
  const api_url =
    "https://openapi.naver.com/v1/search/book.json?query=" +
    encodeURI(req.query.query);
  axios
    .get(api_url, {
      headers: {
        "X-Naver-Client-Id": config.naverBook.clientID,
        "X-Naver-Client-Secret": config.naverBook.clientSecret,
      },
    })
    .then((data) => {
      res.send(data.data.items);
    })
    .catch((err) => next(err));
}

imageRouter.get("/", imageSearch);

그리고 config 파일에서 clientIDclientSecret을 설정해주고, app 파일에서 imageRouter를 등록해주면 백에서는 작업 끝!


그리고 프론트에서는

  1. query 값으로 사용자가 검색창에 입력한 검색어를 넣어주고 imageRouter에 해당하는 url로 GET 요청
  2. 해당 검색어로 검색된 결과를 화면에 렌더
  3. 내가 원하는 책을 클릭하면 해당 item이 갖고있는 image url을 state에 저장
  4. 독서 기록 POST 요청을 보낼 때 title(제목), content(내용)과 함께 url 값을 서버에 전달

위와 같은 방법으로 책 이미지 검색 기능을 구현했다!

// imageSearchModal.tsx
type ImageSearchModalProps = {
  setBookImageUrl: Dispatch<SetStateAction<string>>;
  setModalState: Dispatch<SetStateAction<boolean>>;
};

type BookInfo = { [key: string]: string };

const ImageSearchModal = (props: ImageSearchModalProps) => {
  const [bookSearchKeyword, setbookSearchKeyword] = useState("");
  const [bookSearchResult, setbookSearchResult] = useState(Array<BookInfo>);
  const handleImageSearchInputChange = (
    e: React.ChangeEvent<HTMLInputElement>,
  ) => {
    setbookSearchKeyword(e.target.value);
  };

  const handleImageSearchClick = async () => {
    try {
      if (bookSearchKeyword === "") {
        console.log("검색어 없음");
        return;
      }
      const { data } = await axios.get(`/api/image?query=${bookSearchKeyword}`);
      setbookSearchResult([...data]);
    } catch (err) {
      console.log(err);
    }
  };

  return (
    <Modal title="이미지 검색하기" setModalState={props.setModalState}>
      <ImgSearchModalWrap>
        <ImgSearchBar>
          <ImgSearchInput
            name="img"
            id="img"
            placeholder="책 제목, 지은이, 키워드로 검색할 수 있습니다."
            value={bookSearchKeyword}
            onChange={handleImageSearchInputChange}
          />
          <MyButton btntype="basic" onClick={handleImageSearchClick}>
            검색
          </MyButton>
        </ImgSearchBar>
        {bookSearchResult.map((item, index) => (
          <ImageSearchResult
            key={index}
            item={item}
            setBookImageUrl={props.setBookImageUrl}
            setModalState={props.setModalState}
          />
        ))}
      </ImgSearchModalWrap>
    </Modal>
  );
};

export default ImageSearchModal;
// imageSearchResult.tsx
type BookItem = {
  item: { [key: string]: string };
  setBookImageUrl: Dispatch<SetStateAction<string>>;
  setModalState: Dispatch<SetStateAction<boolean>>;
};

const ImageSearchResult = ({
  item,
  setBookImageUrl,
  setModalState,
}: BookItem) => {
  const handleSearchResultItemClick = () => {
    setBookImageUrl(item.image);
    setModalState(false);
  };
  return (
    <SearchResultItem onClick={handleSearchResultItemClick}>
      <div>
        <SearchResultBookImg src={item.image} />
      </div>
      <SearchResultItemDetail>
        <SearchResultItemTitle>
          {item.author},{item.title}
        </SearchResultItemTitle>
        <SearchResultItemDescription>
          {item.description}
        </SearchResultItemDescription>
      </SearchResultItemDetail>
    </SearchResultItem>
  );
};

export default ImageSearchResult;

그래서 완성된 결과!


의문이 남는 점 + 아쉬운 점을 적어보자면,

  • 검색 모달창 상태를 바꾸는 setIsImageSearchModalOpen 함수와 이미지 url 상태값 받아서 바꾸는 setBookImageUrl 함수를 NewContent 컴포넌트에서 ImageSearchModal 컴포넌트를 거쳐 ImageSearchResult 컴포넌트까지 prop으로 내려주고 있는데 이것도 props drilling이라고 할 수 있는지?
  • 다른 백엔드 router에서는 util에 있는 asyncHandler를 사용해서 요청과 에러처리를 해주는데 나는 그냥 then과 catch만 사용해서.. 이건 사용자가 검색어에 빈 값을 입력했을 때는 어떻게 보여줘야 할지 생각하다가 발견한 문제라서 다른 코드들을 참고해서 에러 처리를 수정해줘야 할 것 같다.

여담으로는, 아주 예전에 네이버 영화 검색 기능을 구현하다가 너무 어려워서 결국 포기했던 기억이 있는데 이번에 이렇게 책 api를 사용해서 요청을 보내고 응답을 받고 그걸 활용해서 기능을 구현할 수 있었다는 거 자체가 너무 감격이었다..😭 다음에는 책 말고 뉴스나 지역 관련된 api도 꼭 활용해보고 싶다!💪

profile
차근차근 천천히

0개의 댓글