웹툰 추천 서비스 제작 과정

김민찬·2022년 7월 16일
0

React Project

목록 보기
2/2

깃허브 주소 : https://github.com/whara123/mbti-webtoon-test

크롤링을 사용해보고 싶어서 mbti 테스트를 기반으로 레진 코믹스 웹툰 추천 서비스를 만들었다.

같이 스터디하는 분이 흥미가 있으시다고 해서 2인 팀으로 진행하게 됐다 !

메인/mbti테스트 페이지를 팀원분이 맡고 나는 원래 목적인 크롤링이 사용되는 결과페이지를 맡게 되었다.

페이지 컨셉을 정하다가, 레진 코믹스를 검색하면 나오는 "재미라는 것이 폭발한다"를 보고 폭발에 꽂혀서 폭발로 컨셉을 정했다.

그렇게 지어진 이름..

크롤링

처음에는 axios와 cheerio를 이용해서 크롤링하면 쉽게 될 줄 알고 시도했는데, 레진은 정적인 페이지가 아니고 js를 통해 동적으로 내용이 생성되기 때문에 axios로는 빈칸만 가져왔다.

동적 페이지 크롤링을 위해 puppeteer를 사용했다.

Chrome 또는 Chromium을 제어하는 고급 API를 제공하는 노드 라이브러리
https://pptr.dev/

puppeteer는 가상 브라우저를 열어서 그 안에서 다양한 작업을 명령할 수 있다.

const puppeteer = require('puppeteer');
const cheerio = require('cheerio');

cheerio는 여전히 요소를 찾을 때 쉽게 사용할 수 있어서 같이 사용했다.

const browser = await puppeteer.launch({headless: true})
const page = await browser.newPage();

//설정한 url로 이동
await page.goto(url);

//페이지 html정보를 가져옴
const content = await page.content();
const $ = cheerio.load(content);

//가져올 태그 선택
const list = $('레진페이지에서 태그 위치 확인').children('li:nth-child(-n+10)');

goto(주소)로 이동 후, 원하는 영역의 seleter를 찾아 그 자식요소 중 li를 상위 10개까지만 가져왔다. 그 뒤 10개의 정보를 each로 돌리면서 배열에 넣어준다.

list.each((idx, node) => {
      recommendData.push({
        title: $(node).find('....').text(),
        artist: $(node).find('....').text(),
        link: $(node).find('a').attr('href'),
        img: $(node).find('... > img').attr('src'),
      });
    });

제목과 작가 정보는 text()로 태그에 있는 텍스트를 가져왔고, 링크나 썸네일 이미지 주소는 attr을 이용해 가져온다.

그런데 문제가 있었다. 동적 페이지인 레진은 아직 드래그되지 않은 영역의 요소들은 로딩이 바로 안되고 어트리뷰트가 src가 아니라 data-src로 적용되어 있어 10개를 제대로 가져오지 못한다.

await page.evaluate(`window.scrollTo(0, document.body.scrollHeight/5)`);

그래서 evaluate을 이용해 페이지 스크롤 위치를 조금 변경했다. 그럼 진입 후 document.body.scrollHeight/5 만큼 살짝 스크롤을 이동하고 레진 페이지가 동작하면서 제대로 된 주소를 가져올 수 있게 된다.

비슷한 동작으로 실시간 랭킹페이지로도 이동해서 상위권 리스트도 가져왔다!

await browser.close();
return { recommendRandomData, rankData };

크롤링이 끝나면 브라우저를 꼭 닫아주자.

크롤링 데이터를 이제 react 페이지로 옮기고 싶은데 나는 node에 상당히 무지한 상태였다.

React에서 export하고 import하면 될줄 알았는데 그런게 아니었다.

이틀을 뻘겋게 뜨는 오류들과 싸우다가 알게되었는데,

브라우저는 puppeteer를 작동시킬 수 없었다

(생각해보니까 당연한거 아녀 .. ?)

express로 구축한 웹서버에서 puppeteer를 사용할 수 있다. 부랴부랴 서버 폴더를 생성해서 express 설치.

서버에서 크롤링을 사용할 수 있게

module.exports.crawler = crawler;

exports해준다.

서버 index.js에서

const express = require('express');
const router = express.Router();

const app = express();
const port = process.env.PORT || 5000;
app.listen(port);

let getCrawler = require('../crawler');

크롤러를 가져오고,

app.use('/getData', async function (req, res) {
  let result;

  if (req.query.name) {
    result = await getCrawler.crawler(req.query.name);
  } else {
    result = await getCrawler.crawler('all');
  }
  console.log(JSON.stringify(result));
  res.send(result);
});

/getData 로 접속해서 크롤러를 사용해준다.

리액트에서 3000번, 서버에서 5000번을 사용해야 해서 port를 5000번으로 넣어줬다.

ProxyMiddleware를 사용해서 cors 이슈를 방지해준다.

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

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

/getData로 api요청 시 http://localhost:5000/getData로 호출하게한다.

서버에서 package.json에 서버와 리액트를 동시에 틀기위해 설정을 바꾼다.
concurrently가 있어야 동시에 실행이 가능하다 !

    "start": "nodemon app.js",
    "dev": "concurrently \"yarn run dev:server\" \"yarn run dev:client\"",
    "dev:server": "yarn start",
    "dev:client": "cd .. && yarn start"

그 뒤에 결과 페이지에서 mbti결과를 받아서 어떤 링크로 이동시킬 지 설정해준다.

const resultData = (result) => {
    fetch(`/getData?name=${result}`)
      .then((res) => {
        return res.json();
      })
      .then((data) => {
        dispatch(
          updateRecommned(data.recommendRandomData, data.rankData, true),
        );
      });
  };

받아온 결과데이터는 redux를 사용하여 관리해준다.

redux

스토어에 resultdata 등록

import { createStore, combineReducers } from 'redux';
import mbtiqna from './modules/mbtiqna';
import mbti from './modules/mbti'
import resultdata from './modules/resultdata';

const rootReducer = combineReducers({ mbtiqna, mbti, resultdata });
const store = createStore(rootReducer);

export default store;

resultdata.js에서 action 생성

const UPDATE = 'resultdata/UPDATE';

export function updateRecommned(rcData, rankData, isLoading) {
  return { type: UPDATE, rcData, rankData, isLoading };
}

export default function reducer(state = {}, action = {}) {
  switch (action.type) {
    case 'resultdata/UPDATE': {
      return {
        rcData: action.rcData,
        rankData: action.rankData,
        isLoading: action.isLoading,
      };
    }
    default:
      return state;
  }
}

이런식으로 추천할 데이터와 실시간 랭킹 데이터 크롤링 완료 여부를 판단하는 객체 상태를 리덕스로 관리한다.

import { useSelector, useDispatch } from 'react-redux';
const dispatch = useDispatch();
.
.
.
dispatch(updateRecommned(data.recommendRandomData, data.rankData, true)
.
.
. 

프로젝트 팀원분이 redux를 익혀두셔서 나도 배우면서 쉽게 사용할 수 있었다 !

받아온 데이터를 통해 react 화면구성에 사용해주었다. 추천 웹툰과 하단에는 실시간 데이터를 넣어준다.

그 뒤에 express는 heroku를 통해, 페이지는 netlify를 이용해서 배포해주었다.

배포 페이지
https://mbtiwebtoontest.netlify.app

처음에는 크롤링만 되면 금방 할 수 있겠지 싶었는데 예상치 못하게 express도 경험해보고 프론트엔드도 서버를 알아야 한다고 하는 이유를 체감하게 됐다.

여기 서술한거보다 오류가 엄청 많았고 하나하나 쉬운게 없었다.

그래도 잘 작동하는 걸 보면 뿌듯하고 재밌는 프로젝트 였다.

profile
프론트엔드 개발자로 나아가고 있는 김민찬입니다.

0개의 댓글