네이버웹툰과 카카오페이지로 웹툰 데이터를 가져오기 위해서 puppeteer와 cheerio를 통해 크롤링을 한다. 먼저 크롤링을 할 때 주의할 점이 있다.
- 로그인
네이버웹툰이나 카카오페이지는 성인인증이 필요한 웹툰들이 존재하고 그 웹툰 데이터를 모두 불러오기 위해 로그인이 필요하다.
- 시간 초과
크롤링할 때 가장 문제점이 페이지를 로딩하는 과정에서 timeout 예외가 발생하고 이에 잘 대응을 하는 것이 중요하다.
- 임시 저장
크롤링할 때 예외가 발생하거나 하는 이유로 크롤링이 도중에 멈추는 경우도 있는데 이미 가져온 데이터만이라도 저장을 하고 다음에는 예외가 발생한 부분부터 다시 시작할 필요가 있다.
$ npm install puppeteer cheerio
$ npm install -D @types/puppeteer @types/cheerio
│ app.controller.ts
│ app.module.ts
│ app.service.ts
│ main.ts
│
├─auth
│ │ auth.controller.ts
│ │ auth.module.ts
│ │ auth.service.ts
│ │
│ ├─guard
│ │ accessToken.guard.ts
│ │ refreshToken.guard.ts
│ │
│ └─strategy
│ accessToken.strategy.ts
│ refreshToken.strategy.ts
│
├─caching
│ caching.module.ts
│
├─config-project
│ config-project.module.ts
│
├─constatns
│ cache.constants.ts
│ crawling.constants.ts
│
├─crawling
│ │ crawling.controller.ts
│ │ crawling.module.ts
│ │ crawling.service.ts
│ │
│ └─functions
│ ├─kakao
│ │ pageLogin.function.ts
│ │
│ └─naver
│ pageLogin.function.ts
│
├─custom-provider
│ filter.provider.ts
│ model.provider.ts
│
├─dto
│ auth.dto.ts
│ dtoFunction.ts
│ user.dto.ts
│ webtoon.dto.ts
│
├─exception-filter
│ dtoException.filter.ts
│
├─sequelize
│ │ mysql_sequelize.module.ts
│ │
│ ├─config
│ │ config.json
│ │
│ ├─entity
│ │ user.model.ts
│ │ userWebtoon.model.ts
│ │ webtoon.model.ts
│ │
│ ├─migrations
│ ├─models
│ │ index.js
│ │
│ └─seeders
├─types
│ auth.type.ts
│ user.type.ts
│ webtoon.type.ts
│
├─user
│ user.controller.ts
│ user.module.ts
│ user.service.ts
│
└─webtoon
webtoon.controller.ts
webtoon.module.ts
webtoon.service.ts
이번 리팩토링에서 중점으로 두는 것은 코드의 확장성 및 재사용성이다.
[crawling.service.ts]
...
async login(page: Page, service: string): Promise<boolean> {
const id = this.configService.get<string>(`CRAWLING_${service.toUpperCase()}_ID`);
const pw = this.configService.get<string>(`CRAWLING_${service.toUpperCase()}_PW`);
const loginResult = await(
service === "naver" ? naverPageLogin(page, id, pw) : kakaoPageLogin(page, id, pw)
);
return loginResult;
}
...
[pageLogin.function.ts]
import { Page } from "puppeteer";
import { KAKAO_LOGIN_HOME_URL, KAKAO_LOGIN_SELECTOR, KAKAO_LOGIN_SUBMIT_BUTTON } from "src/constatns/crawling.constants";
export async function kakaoPageLogin(page: Page, id: string, pw: string) {
try {
// 로그인 페이지 이동
await page.goto(KAKAO_LOGIN_HOME_URL);
await page.waitForSelector(KAKAO_LOGIN_SELECTOR);
await page.click(KAKAO_LOGIN_SELECTOR);
await page.waitForSelector(KAKAO_LOGIN_SUBMIT_BUTTON);
// 권한 요청 창 방지
page.on('dialog', async (dialog) => {
console.log(dialog.message());
await dialog.accept();
});
// 로그인 아이디 비번 입력
await page.type("#loginId--1", id);
await page.type("#password--2", pw);
// 로그인 버튼 클릭
await page.click(KAKAO_LOGIN_SUBMIT_BUTTON);
// 페이지 이동이 두번이기 때문에 waitFor 두번
await page.waitForNavigation();
await page.waitForNavigation();
// 로그인 성공 여부 체크
if (page.url() === KAKAO_LOGIN_HOME_URL) {
return true;
}
} catch (e) {
console.log(e);
}
return false;
}
import { Page } from "puppeteer";
import {
NAVER_LOGIN_HOME_URL,
NAVER_LOGIN_PAGE_BUTTON,
NAVER_LOGIN_SUBMIT_BUTTON,
} from "src/constatns/crawling.constants";
export async function naverPageLogin(page: Page, id: string, pw: string) {
try {
// 네이버 홈페이지 이동
await page.goto(NAVER_LOGIN_HOME_URL);
await page.waitForSelector(NAVER_LOGIN_PAGE_BUTTON);
// 로그인 페이지로 접속
await page.click(NAVER_LOGIN_PAGE_BUTTON);
await page.waitForSelector(NAVER_LOGIN_SUBMIT_BUTTON);
// 1초동안 id, pw를 입력하기(매크로 방지 뚫기)
await page.click("#id");
await page.keyboard.type(id, { delay: 1000 });
await page.click("#pw");
await page.keyboard.type(pw, { delay: 1000 });
// 로그인 버튼 클릭후 잠시 대기
await page.click(NAVER_LOGIN_SUBMIT_BUTTON);
await page.waitForTimeout(1000);
// 홈페이지로 돌아와진다면 로그인 성공
if (page.url() === NAVER_LOGIN_HOME_URL) {
return true;
}
} catch (e) {
console.log(e);
}
return false;
}
플랫폼이 현재 kakao, naver 두개이고, 추후에 더 추가될 가능성도 있다. 만약 로그인 플랫폼이 추가된다면? 나는 해당 플랫폼의 로그인 기능 함수를 구현하고 환경변수를 더해준 뒤에 다음의 코드만 수정 해주면 될 것이다.
const loginResult = await(
service === "naver" ? naverPageLogin(page, id, pw) : kakaoPageLogin(page, id, pw)
);
네이버와 카카오 둘 다 각 웹툰들의 id가 있다. 이번엔 그 id로 해당 웹툰의 데이터를 가져오는 메서드와 함수들을 작성할 것이다. 또한 해당 웹툰의 특정 정보(업데이트 날짜, 장르, ...)만 불러오고 싶다면 그 정보만 크롤링을 해서 속도를 더 향상 시킬 예정이다.
[crawling.service.ts]
...
async crawlWebtoonForId(page: Page, webtoonId: string, service: string): Promise<CrawledWebtoon> {
const webtoon = service === "naver"
? await getNaverWebtoonForId(page, webtoonId)
: await getKakaoWebtoonForId(page, webtoonId);
return webtoon;
}
...
import { Page } from "puppeteer";
import * as cheerio from "cheerio";
import { CrawlOption, CrawledWebtoon } from "src/types/webtoon.interface";
import {
KAKAO_AUTHOR_SELECTOR,
KAKAO_CATEGORY_SELECTOR,
KAKAO_DESCRIPTION_SELECTOR,
KAKAO_EPISODELENGTH_CLICK_SELECTOR,
KAKAO_EPISODELENGTH_SELECTOR,
KAKAO_EPISODELENGTH_WAIT_SELECTOR,
KAKAO_FANCOUNT_SELECTOR,
KAKAO_GENRE_SELECTOR,
KAKAO_THUMBNAIL_SELECTOR,
KAKAO_TITLE_SELECTOR,
KAKAO_UPDATEDAY_SELECTOR,
} from "src/constatns/crawling.constants";
function parseIntFromFanCountText(fanCountText: string) {
const replacedCount = fanCountText.replaceAll(',', "");
// 만, 억과 같은 문자열을 숫자로 변환
let fanCount = parseFloat(replacedCount);
// "만", "억" 등의 문자열을 처리
if (fanCountText.includes('만')) {
fanCount *= 10000; // 만(10,000)을 곱함
} else if (fanCountText.includes('억')) {
fanCount *= 100000000; // 억(100,000,000)을 곱함
}
return fanCount;
}
export async function getKakaoWebtoonForId(
page: Page,
webtoonId: string,
option?: CrawlOption,
): Promise<CrawledWebtoon> {
const webtoon: CrawledWebtoon = { webtoonId, service: "kakao" };
try {
await page.goto(`https://page.kakao.com/content/${webtoonId}?tab_type=about`);
await page.waitForSelector(KAKAO_DESCRIPTION_SELECTOR);
let content = await page.content();
let $ = cheerio.load(content);
let rootElement: cheerio.Cheerio;
// 제목 크롤링
if (!option || option.title) {
rootElement = $(KAKAO_TITLE_SELECTOR);
const title = rootElement.first().text();
webtoon.title = title;
}
// 작가 크롤링
if (!option || option.author) {
rootElement = $(KAKAO_AUTHOR_SELECTOR);
const author = rootElement.first().text().split(",");
webtoon.author = JSON.stringify(author);
}
// 카테고리 크롤링
if (!option || option.category) {
rootElement = $(KAKAO_CATEGORY_SELECTOR);
const category = rootElement.last().text();
webtoon.category = category;
}
// 조회수 크롤링
if (!option || option.fanCount) {
rootElement = $(KAKAO_FANCOUNT_SELECTOR);
const fanCountText = rootElement.last().text().replaceAll(",", "");
const fanCount = parseIntFromFanCountText(fanCountText);
webtoon.fanCount = fanCount;
}
// 업데이트 날짜 크롤링
if (!option || option.updateDay) {
rootElement = $(KAKAO_UPDATEDAY_SELECTOR);
const updateDay = rootElement.first().text().charAt(0);
webtoon.updateDay = updateDay;
}
// 썸네일 크롤링
if (!option || option.thumbnail) {
rootElement = $(KAKAO_THUMBNAIL_SELECTOR);
const thumbnail = rootElement.attr("src");
webtoon.thumbnail = thumbnail;
}
// 장르 키워드 크롤링
if (!option || option.genres) {
rootElement = $(KAKAO_GENRE_SELECTOR);
const genres: string[] = [ webtoon.category ];
rootElement.children().map((idx, element) => {
const t_obj: string = $(element).attr("data-t-obj");
const genre = t_obj ? JSON.parse(t_obj).click.copy : null;
if (genre) genres.push(genre);
});
const genreCount = genres.length;
webtoon.genres = JSON.stringify(genres);
webtoon.genreCount = genreCount;
}
// 줄거리 크롤링
if (!option || option.description) {
rootElement = $(KAKAO_DESCRIPTION_SELECTOR);
const description = rootElement.text();
webtoon.description = description;
}
if (!option || option.episodeLength || option.fanCount) {
// 페이지 이동 (에피소드 개수 크롤링 하기 위해)
await page.click(KAKAO_EPISODELENGTH_CLICK_SELECTOR);
await page.waitForSelector(KAKAO_EPISODELENGTH_WAIT_SELECTOR);
// 페이지를 이동했기 때문에 다시 페이지 내용 불러오기
content = await page.content();
$ = cheerio.load(content);
// 에피소드 개수 크롤링
rootElement = $(KAKAO_EPISODELENGTH_SELECTOR);
const episodeLength = parseInt(rootElement.text().split(" ")[1]);
webtoon.episodeLength = episodeLength;
}
// 팬수 구하기 (전체 조회수 / 에피소드 개수)
if (!option || option.fanCount) {
webtoon.fanCount = Math.floor(webtoon.fanCount / webtoon.episodeLength);
}
} catch(e) {
console.log(`webtoonId ${webtoonId} is not crawled..`);
return null;
}
return webtoon;
}
import { Page } from "puppeteer";
import * as cheerio from "cheerio";
import { CrawlOption, CrawledWebtoon } from "src/types/webtoon.interface";
import {
NAVER_AUTHOR_SELECTOR,
NAVER_DESCRIPTION_SELECTOR,
NAVER_EPISODELENGTH_SELECTOR,
NAVER_EPISODELENGTH_WAIT_SELECTOR,
NAVER_FANCOUNT_SELECTOR,
NAVER_GENRE_SELECTOR,
NAVER_THUMBNAIL_SELECTOR,
NAVER_TITLE_SELECTOR,
NAVER_UPDATEDAY_SELECTOR,
} from "src/constatns/crawling.constants";
export async function getNaverWebtoonForId(
page: Page,
webtoonId: string,
option?: CrawlOption,
): Promise<CrawledWebtoon> {
const webtoon: CrawledWebtoon = { webtoonId, service: "NAVER" };
try {
await page.goto(`https://comic.naver.com/webtoon/list?titleId=${webtoonId}`);
await page.waitForSelector(NAVER_EPISODELENGTH_WAIT_SELECTOR);
let content = await page.content();
let $ = cheerio.load(content);
let rootElement: cheerio.Cheerio;
// 제목 크롤링
if (!option || option.title) {
rootElement = $(NAVER_TITLE_SELECTOR);
const title = rootElement.first().text();
webtoon.title = title;
}
// 작가 크롤링
if (!option || option.author) {
rootElement = $(NAVER_AUTHOR_SELECTOR);
const author: string[] = [];
rootElement.map((idx, element) => {
const $data = cheerio.load(element);
const author_ = $data("a").text()
author.push(author_);
});
webtoon.author = JSON.stringify(author);
}
// 조회수 크롤링
if (!option || option.fanCount) {
rootElement = $(NAVER_FANCOUNT_SELECTOR);
const fanCountText = rootElement.last().text().replaceAll(",", "");
const fanCount = parseInt(fanCountText);
webtoon.fanCount = fanCount;
}
// 업데이트 날짜 크롤링
if (!option || option.updateDay) {
rootElement = $(NAVER_UPDATEDAY_SELECTOR);
const updateDayText = rootElement.first().text();
const updateDay = parseInt(updateDayText)
? "휴"
: updateDayText.includes("완결")
? "완"
: updateDayText.charAt(0);
webtoon.updateDay = updateDay;
}
// 썸네일 크롤링
if (!option || option.thumbnail) {
rootElement = $(NAVER_THUMBNAIL_SELECTOR);
const thumbnail = rootElement.attr("src");
webtoon.thumbnail = thumbnail;
}
// 장르 키워드 크롤링
if (!option || option.genres || option.category) {
rootElement = $(NAVER_GENRE_SELECTOR);
const genres: string[] = [];
rootElement.children().map((idx, element) => {
const $data = cheerio.load(element);
const genre: string = $data("a").text().replace("#", "");
genres.push(genre);
});
const genreCount = genres.length;
webtoon.category = genres[0];
webtoon.genres = JSON.stringify(genres);
webtoon.genreCount = genreCount;
}
// 줄거리 크롤링
if (!option || option.description) {
rootElement = $(NAVER_DESCRIPTION_SELECTOR);
const description = rootElement.text();
webtoon.description = description;
}
// 에피소드 개수 크롤링
if (!option || option.episodeLength || option.fanCount) {
rootElement = $(NAVER_EPISODELENGTH_SELECTOR);
const episodeLength = parseInt(rootElement.text().split(" ")[1]);
webtoon.episodeLength = episodeLength;
}
} catch(e) {
console.log(`webtoonId ${webtoonId} is not crawled..`);
return null;
}
return webtoon;
}
export const NAVER_LOGIN_HOME_URL = "https://comic.naver.com/index";
export const NAVER_LOGIN_PAGE_BUTTON = "#gnb_login_button";
export const NAVER_LOGIN_SUBMIT_BUTTON = "button[type=submit]";
export const KAKAO_LOGIN_HOME_URL = "https://page.kakao.com/";
export const KAKAO_LOGIN_SELECTOR = "#__next > div > div.sticky.inset-x-0.top-0.left-0.z-100.flex.w-full.flex-col.items-center.justify-center.bg-bg-a-10 > div > div.flex.h-pc_header_height_px.w-1200pxr.items-center.px-30pxr > div.mr-16pxr.flex.shrink-0.items-center.justify-end.space-x-24pxr > button.pr-16pxr";
export const KAKAO_LOGIN_SUBMIT_BUTTON = "button[type=submit]";
export const NAVER_TITLE_SELECTOR = "#content > div.EpisodeListInfo__comic_info--yRAu0 > div > h2";
export const NAVER_AUTHOR_SELECTOR = "#content > div.EpisodeListInfo__comic_info--yRAu0 > div > div.ContentMetaInfo__meta_info--GbTg4 > span";
export const NAVER_FANCOUNT_SELECTOR = "#content > div.EpisodeListView__user_wrap--S_pYn > div > button.EpisodeListUser__item--Fjp4R.EpisodeListUser__favorite--DzoPt > span.EpisodeListUser__count--fNEWK";
export const NAVER_UPDATEDAY_SELECTOR = "#content > div.EpisodeListInfo__comic_info--yRAu0 > div > div.ContentMetaInfo__meta_info--GbTg4 > em";
export const NAVER_THUMBNAIL_SELECTOR = "#content > div.EpisodeListInfo__comic_info--yRAu0 > button > div > img";
export const NAVER_GENRE_SELECTOR = "#content > div.EpisodeListInfo__comic_info--yRAu0 > div > div.EpisodeListInfo__summary_wrap--ZWNW5 > div > div";
export const NAVER_DESCRIPTION_SELECTOR = "#content > div.EpisodeListInfo__comic_info--yRAu0 > div > div.EpisodeListInfo__summary_wrap--ZWNW5 > p";
export const NAVER_EPISODELENGTH_SELECTOR = "#content > div.EpisodeListView__episode_list_wrap--q0VYg > div.EpisodeListView__episode_list_head--PapRv > div.EpisodeListView__count--fTMc5";
export const NAVER_EPISODELENGTH_WAIT_SELECTOR = "#content > div.EpisodeListView__episode_list_wrap--q0VYg > ul";
export const KAKAO_TITLE_SELECTOR = "#__next > div > div.flex.w-full.grow.flex-col.px-122pxr > div.flex.h-full.flex-1 > div.mb-28pxr.flex.w-320pxr.flex-col > div.rounded-t-12pxr.bg-bg-a-20 > div > div.relative.px-18pxr.text-center.bg-bg-a-20.mt-24pxr > a > div > span.font-large2-bold.mb-3pxr.text-ellipsis.break-all.text-el-70.line-clamp-2";
export const KAKAO_AUTHOR_SELECTOR = "#__next > div > div.flex.w-full.grow.flex-col.px-122pxr > div.flex.h-full.flex-1 > div.mb-28pxr.flex.w-320pxr.flex-col > div.rounded-t-12pxr.bg-bg-a-20 > div > div.relative.px-18pxr.text-center.bg-bg-a-20.mt-24pxr > a > div > span.font-small2.mb-6pxr.text-ellipsis.text-left.text-el-70.opacity-70.break-word-anywhere.line-clamp-2";
export const KAKAO_CATEGORY_SELECTOR = "#__next > div > div.flex.w-full.grow.flex-col.px-122pxr > div.flex.h-full.flex-1 > div.mb-28pxr.flex.w-320pxr.flex-col > div.rounded-t-12pxr.bg-bg-a-20 > div > div.relative.px-18pxr.text-center.bg-bg-a-20.mt-24pxr > a > div > div.flex.h-16pxr.items-center.justify-center.all-child\\:font-small2.\\[\\&\\>\\*\\:not\\(\\:last-child\\)\\]\\:mr-10pxr > div:nth-child(1) > div > span";
export const KAKAO_FANCOUNT_SELECTOR = "#__next > div > div.flex.w-full.grow.flex-col.px-122pxr > div.flex.h-full.flex-1 > div.mb-28pxr.flex.w-320pxr.flex-col > div.rounded-t-12pxr.bg-bg-a-20 > div > div.relative.px-18pxr.text-center.bg-bg-a-20.mt-24pxr > a > div > div.flex.h-16pxr.items-center.justify-center.all-child\\:font-small2.\\[\\&\\>\\*\\:not\\(\\:last-child\\)\\]\\:mr-10pxr > div:nth-child(2) > span";
export const KAKAO_UPDATEDAY_SELECTOR = "#__next > div > div.flex.w-full.grow.flex-col.px-122pxr > div.flex.h-full.flex-1 > div.mb-28pxr.flex.w-320pxr.flex-col > div.rounded-t-12pxr.bg-bg-a-20 > div > div.relative.px-18pxr.text-center.bg-bg-a-20.mt-24pxr > a > div > div.mt-6pxr.flex.items-center > span";
export const KAKAO_THUMBNAIL_SELECTOR = "#__next > div > div.flex.w-full.grow.flex-col.px-122pxr > div.flex.h-full.flex-1 > div.mb-28pxr.flex.w-320pxr.flex-col > div.rounded-t-12pxr.bg-bg-a-20 > div > div.relative.overflow-hidden.h-326pxr.w-320pxr.pt-86pxr > div.relative.h-full.min-h-\\[inherit\\] > div.mx-auto.w-160pxr > div > div > img";
export const KAKAO_GENRE_SELECTOR = "#__next > div > div.flex.w-full.grow.flex-col.px-122pxr > div.flex.h-full.flex-1 > div.mb-28pxr.ml-4px.flex.w-632pxr.flex-col.overflow-hidden.rounded-12pxr > div.flex.flex-1.flex-col > div > div:nth-child(1) > div:nth-child(3) > div.flex.w-full.flex-col.items-center.overflow-hidden > div";
export const KAKAO_DESCRIPTION_SELECTOR = "#__next > div > div.flex.w-full.grow.flex-col.px-122pxr > div.flex.h-full.flex-1 > div.mb-28pxr.ml-4px.flex.w-632pxr.flex-col.overflow-hidden.rounded-12pxr > div.flex.flex-1.flex-col > div > div:nth-child(1) > div:nth-child(2) > div.flex.w-full.flex-col.items-center.overflow-hidden > div > div > span";
export const KAKAO_EPISODELENGTH_CLICK_SELECTOR = "#__next > div > div.flex.w-full.grow.flex-col.px-122pxr > div.flex.h-full.flex-1 > div.mb-28pxr.ml-4px.flex.w-632pxr.flex-col.overflow-hidden.rounded-12pxr > div.relative.flex.w-full.flex-col.my-0.bg-bg-a-20.px-15pxr.pt-28pxr.pb-12pxr > div > div > div:nth-child(1)";
export const KAKAO_EPISODELENGTH_WAIT_SELECTOR = "#__next > div > div.flex.w-full.grow.flex-col.px-122pxr > div.flex.h-full.flex-1 > div.mb-28pxr.ml-4px.flex.w-632pxr.flex-col.overflow-hidden.rounded-12pxr > div.flex-1.flex.flex-col > div.rounded-b-12pxr.bg-bg-a-20 > div.flex.min-h-\\[250px\\].flex-col.justify-center > div.min-h-364pxr > ul";
export const KAKAO_EPISODELENGTH_SELECTOR = "#__next > div > div.flex.w-full.grow.flex-col.px-122pxr > div.flex.h-full.flex-1 > div.mb-28pxr.ml-4px.flex.w-632pxr.flex-col.overflow-hidden.rounded-12pxr > div.flex-1.flex.flex-col > div.rounded-b-12pxr.bg-bg-a-20 > div.flex.h-44pxr.w-full.flex-row.items-center.justify-between.bg-bg-a-20.px-18pxr > div.flex.h-full.flex-1.items-center.space-x-8pxr > span";
crawling 함수를 작성할때 중점으로 생각한건 속도, 재사용성, 가독성이다.
크롤링할때 크롤링 하고싶은 페이지에서 f12 개발자도구를 켜고 선택자를 복사해서 붙여넣는다. 하지만 오류가 발생한 부분은 선택자 안에 \가 있었다. 즉 그대로 문자열 안에 넣으면 cheerio는 선택자를 찾을수 없고 \로 수정을 해줘야 한다.
카카오페이지를 크롤링 하는 도중에 페이지를 이동해야 하는 일이 발생했다. 그리고 이동한 페이지에서 선택자를 통해 크롤링을 시도했지만 오류가 발생하지도 않고 그냥 빈 값만 돌아왔다. 오류 정보도 없이 생각대로 코드가 돌아가지 않아 코드를 처음부터 끝까지 다시 읽으며 로직을 생각했다.
puppeteer와 cheerio를 통해 크롤링할때 다음과 같은 코드를 처음 입력한다.
let content = await page.content();
let $ = cheerio.load(content);
위의 코드는 현재 페이지의 내용물을 cheerio로 로딩하는 것이다. 즉 만약 페이지가 이동을 해서 페이지의 내용이 바뀌었다면 위와 같은 페이지의 내용을 로딩하는 과정을 다시 한번 해주어야 한다.
리팩토링 전에는 코드를 확장시에 플랫폼에 해당하는 크롤링 함수 뿐 아니라 많은 코드를 수정해야 했다.
리팩토링 후에는 삼항문자열에 해당 플랫폼의 내용을 추가하는 코드 두줄씩만 넣어주면된다. 또한 상수를 따로 관리하고 이름의 규칙성도 있기에 코드를 확장하는데 시간 소요가 많이 줄어든다.
매개변수에 CrawlOption을 넣고 조건문을 통해 원하는 정보만 선택해서 크롤링 하는 기능을 도입헀다. 그리고 카카오 웹툰을 크롤링할 때 페이지 이동시에 페이지 내용을 다시 cheerio로 불러오는 방식을 도입했다.
[결과] 카카오 웹툰의 크롤링 시간: 30초 -> 2초
리팩토링 전에는 episodeLength, genre 등 원하는 정보만 선택해서 크롤링을 해야 하는 경우가 생길때(웹툰 전체를 불러오면 시간 소요가 컸음) 새로운 함수를 만들어야 했고 이는 매우 귀찮고 번거로우며 시간 소요가 걸렸다.
리팩토링 후에는 getWebtoonForId함수에 CrawlOption을 넣어 조건문을 통해 어떤 정보를 크롤링할지 선택이 가능하기 때문에 새로운 함수를 만들 필요 없이 이 함수만을 사용할 수 있다.
리팩토링 전에는 URL, SELECTOR들을 모두 함수안에서 상수로 정의하고 사용했다. 어떤 문제가 생길까?
크롤링은 내 사이트가 아니라 다른 사이트에서 정보를 불러온다. 그럼 만약 크롤링을 해오는 원본 사이트가 변경될 경우 내 크롤링 코드도 수정을 해야하는데 만약 선택자가 달라졌다면?
나는 그 선택자를 정의 해놓은 함수들의 파일을 다 찾아가며 하나하나 수정해야 한다. 이는 매우 귀찮고 번거롭다. 또한 실수로 수정을 못하고 넘어가는 경우에 예상치 못한 에러까지 발생한다.
리팩토링 후에는 URL, SELETOR 등은 모두 constants 파일을 따로 만들어 한 곳에서 관리하고, cheerio의 이해도가 낮아 불필요한 코드를 많이 작성했었는데 이를 지우고 규칙적이고 읽기 쉬운 코드로 작성했다.
https://pptr.dev/api/puppeteer.puppeteernode - puppeteer 공식 문서
https://cheerio.js.org/docs/api - cheerio 공식 문서