[React] 무비앱 #2 - 네이버 API + 크롤링으로 검색 기능과 실시간 랭킹 구현

nemo·2022년 2월 17일
3

Toy Project

목록 보기
2/4
post-thumbnail
post-custom-banner

👉 [React] 무비앱 #1 - 네이버 API + 크롤링으로 검색 기능과 실시간 랭킹 구현




3. 영화 랭킹 구현

영화진흥위원회 API를 사용하면 박스 오피스 데이터를 쉽게 가져올 수 있지만, 네이버 영화 웹 페이지를 크롤링하는 방식으로 구현해보겠다.

3-1. Cheerio 세팅

크롤링 작업에는 axios와 cheerio를 사용할 것이다. axios는 이미 설치했으니 cheerio만 설치해준다.

npm install cheerio --save

  • axios: 웹 페이지의 HTML을 가져옴
  • cheerio: 가져온 HTML에서 필요한 정보를 추출(파싱)

3-2. HTML 가져오기

server 폴더 아래에 fetching.js 파일을 생성한다.
cheerio와 axios 모듈을 가져오고, HTML을 가져오는 함수를 작성한다. async와 try-catch문을 사용할 것이다.

(fetching.js)

// 설치한 axios와 cheerio 모듈을 가져온다.
const cheerio = require("cheerio");
const axios = require("axios");

// axios로 HTML을 가져오는 함수
const getHTML = async () => {
  try {
    return await axios.get('https://movie.naver.com/movie/sdb/rank/rmovie.naver')
  } catch (error) {
    console.log(error);
  }
}

네이버 영화 랭킹 페이지의 전체 HTML을 긁어오는 코드를 작성했다.
이제 추출(파싱)까지 해보자. parsing 함수를 생성하고, 함수 안에서 axios로 추출한 HTML을 변수에 담는다.


(fetching.js)

// 설치한 axios와 cheerio 모듈을 가져온다.
const cheerio = require("cheerio");
const axios = require("axios");

// axios로 HTML을 가져오는 함수
const getHTML = async () => {
  try {
    return await axios.get('https://movie.naver.com/movie/sdb/rank/rmovie.naver')
  } catch (error) {
    console.log(error);
  }
}

// 파싱
const parsing = async () => {
  // 위에서 추출한 HTML 전체 가져오기
  const html = await getHTML();

  console.log(html)
}

parsing()

루트 경로에서 node server/fetching.js 명령어를 입력해보자. axios로 긁어온 엄청난 양의 데이터가 터미널에 출력된다.


3-3. 데이터 추출 (파싱)

Cheerio를 통해 저 데이터에서 1위부터 10위까지 영화의 타이틀과 링크를 추출할 것이다. Cheerio는 JQuery 문법을 사용한다.

(fetching.js)

const cheerio = require("cheerio");
const axios = require("axios");

// HTML 가져오기
const getHTML = async () => {
  ...
}

// 파싱
const parsing = async () => {
  // 위에서 추출한 HTML 전체 가져오기
  const html = await getHTML();
  // JQuery처럼 사용하기 위해 '$'에 cheerio를 로드한다.
  const $ = cheerio.load(html.data);
  
  // 개발자 모드에서 확인해보면
  // .list_ranking 아래 tr들이 있고 그 안에 하나씩 타이틀 존재
  // 반복문을 돌릴 수 있어야 하니 병렬로 있는 요소까지만 찾는다.
  const $trs = $("#old_content > .list_ranking > tbody > tr");
 
  // 파싱한 데이터를 담을 배열
  let dataArr = [];

  // 찾은 tr 개수 만큼 반복문을 돌린다.
  $trs.each((idx, node) => {
    const title = $(node).find(".tit3 a").text();
    const link = $(node).find(".tit3 a").attr("href");

    // 빈 값 리턴
    if (title === "") {
      return;
    }

    // 오브젝트 형식으로 배열에 담기
    dataArr.push({
      title: title,
      link: link
    });
  });

  console.log(dataArr)
}

parsing()

서버를 껐다가 다시 켜준다. 아래와 같이 출력되면 성공이다.

3-4. fetching.js 모듈화

이제 fetching.js를 모듈화해서 server/index.js로 넘겨보자.

(fetching.js)

const cheerio = require("cheerio");
const axios = require("axios");

// HTML 가져오기
const getHTML = async () => {
  ...
};

// 파싱
const parsing = async () => {
  ...
};

// parsing 함수를 모듈화해서 내보내기
module.exports = parsing;

(server/index.js)

const express = require("express");
const app = express();
const port = process.env.PORT || 5000;
const parsing = require('./fetching.js');
...
// parsing 모듈 import
const parsing = require('./fetching.js');

...

// api/rank로 get 요청이 들어오면 
// parsing() 실행해서 요청 결괏값 client로 내보내기
app.get('/api/rank', (req, res) => {
  parsing().then(response => res.send(response))
})

...

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});


3-5. 클라이언트로 내보내기

이제 이렇게 받은 랭킹 타이틀을 client에서 받을 수 있게 설정해보자.

URL이 /api로 시작되는 요청은 5000번 포트로 서버가 실행되도록 하는 프록시 작업은 이 전에 이미 해놨기 때문에 패스.

(setupProxy.js)

const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function(app) {
  app.use (
    createProxyMiddleware( '/api', {
      target: 'http://localhost:5000',
      changeOrigin: true
    })
  )
}

(ranking.tsx)

import axios from 'axios'
import React, { useEffect, useState } from 'react'

const Ranking = () => {
  // 랭킹 데이터를 담을 State
  const [Data, setData] = useState([])

  // 랭킹 데이터 가져오는 함수
  const fetchRank = async () => {
    try {
      const { data } = await axios.get('/api/rank')
      // state에 담기
      setData(data)
    } catch (error) {
      let message = 'Unknown Error'
      if (error instanceof Error) message = error.message
      console.log(message);
    }
  }

  // 마운트 시 데이터 가져오는 함수 실행
  useEffect(() => {
    fetchRank()
  }, [])

  return (
    <div className="ranking">
      <div className="ranking__inner">
        <h2>영화 랭킹</h2>
        <div className="ranking__list">
          <div className="ranking__list__inner">
            <ul>
              {
                Data &&
                  Data.map((data: { title: string, link: string }, idx) => (
                  <li key={ idx } >
                    <span className="num">{ idx+1 }</span>
                    <a href={ `https://movie.naver.com/${data.link}` } target="_blank">
                      <span className="title">{ data.title }</span>
                    </a>
                  </li>
                ))
              }
            </ul>
          </div>
        </div>
      </div>
    </div>
  )
}

export default Ranking

4. 마무리

CSS

CSS 한스푼 넣으면 완성이다.


News Picker

썸네일처럼 랭킹 영역을 실시간 검색어 형식(News picker)으로 구현하고 싶다면 아래 코드를 참고해보자.


Lazy-loading

컴포넌트의 불필요한 렌더링을 줄이기 위해 Lazy-loading을 세팅해준다. router v6부터 Lazy-loading을 적용하는 방법은 아래 페이지를 참고하면 된다.

📎 [React] Router v6 - Lazy-loading


마무리까지 완료된 페이지 구경하기

📎 Demo




배포

헤로쿠에 배포하는 방법은 아래 링크에 정리해두었으니 참고하면 되겠다.

📎 MERN 스택 앱 Heroku 배포

post-custom-banner

0개의 댓글