👉 [React] 무비앱 #1 - 네이버 API + 크롤링으로 검색 기능과 실시간 랭킹 구현
영화진흥위원회 API를 사용하면 박스 오피스 데이터를 쉽게 가져올 수 있지만, 네이버 영화 웹 페이지를 크롤링하는 방식으로 구현해보겠다.
크롤링 작업에는 axios와 cheerio를 사용할 것이다. axios는 이미 설치했으니 cheerio만 설치해준다.
npm install cheerio --save
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로 긁어온 엄청난 양의 데이터가 터미널에 출력된다.
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()
서버를 껐다가 다시 켜준다. 아래와 같이 출력되면 성공이다.
이제 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}`);
});
이제 이렇게 받은 랭킹 타이틀을 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
CSS 한스푼 넣으면 완성이다.
썸네일처럼 랭킹 영역을 실시간 검색어 형식(News picker)으로 구현하고 싶다면 아래 코드를 참고해보자.
컴포넌트의 불필요한 렌더링을 줄이기 위해 Lazy-loading을 세팅해준다. router v6부터 Lazy-loading을 적용하는 방법은 아래 페이지를 참고하면 된다.
📎 [React] Router v6 - Lazy-loading
📎 Demo
헤로쿠에 배포하는 방법은 아래 링크에 정리해두었으니 참고하면 되겠다.