서론

지난 포스트에서 프로젝트 세팅을 모두 끝마쳤습니다! 이제 실제로 멋진 Typescript Express 크롤러를 만들어볼 차례입니다. 어서 개발하러 가시죠!

1. 크롤러의 작동원리

크롤러의 작동원리는 사실 정말 별 것도 없습니다. 우리가 일반적으로 웹 서핑을 할 때 보는 웹 페이지들은 다들 알고 계시는 것처럼 HTML과 CSS, 자바스크립트로 동작하게 됩니다. 즉, 다시 말해서 우리가 크롬이나 파이어폭스같은 브라우저로 네이버에 접속하게 되면 브라우저가 네이버 서버로 네이버 페이지를 내놓으라고 HTTP GET 메서드로 요청을 하게 됩니다. 이 때, 네이버 서버는 네이버 웹 페이지를 구성하고 있는 HTML, CSS, 자바스크립트 파일을 보내면 브라우저가 그 파일들을 한 번에 읽어서 우리에게 웹 페이지로 보여주는 거죠.

크롤러.png

결국, 크롤러는 우리가 몰랐던 엄청 대단한 기술이 아니라 그저 우리가 평소에 웹사이트에 접속해서 웹사이트를 둘러보는 일이 바로 크롤러가 하는 일입니다.

2. 크롤러 개발

이제 크롤러를 개발해봅시다! 이번에 만들 크롤러는 /crawl 라우터로 GET 요청을 보내게 되면 현재 실시간 검색어를 응답하도록 만들어 보겠습니다.

express 서버 만들기

// C://projects/my-first-ts-express-croller/src/index.ts
import express from 'express';

const app = express();

// 클라이언트가 crawl 라우터로 GET 요청을 보냈을 때, 'hello'라는 값을 보내는 코드 
app.get('/crawl', (req, res) => res.send('hello'));
app.listen(8080, () => {
  console.log('server started!');
});
// C://projects/my-first-ts-express-croller/src/index.ts
{
  // scripts 이외 생략
  "scripts": {
    "start": "tsc && node dist"
  },
}

package.json의 scripts를 위와 같이 바꾸게 되면 이제 우리는 실행할 수 있는 Express 서버를 가지게 된 것입니다. 이제 npm start를 커맨드 라인에서 입력하게 되면 콘솔엔 server started!라고 출력될 것입니다. 그리고 브라우저로 localhost:8080/으로 접속하게 되면 브라우저에 hello라고 출력됩니다. 그럼 크롤러를 실행시킬 환경이 구축되었으니 크롤링을 해봅시다.

크롤링하기

앞서 말했던 것 처럼 크롤링은 우리가 몰랐던 새로운 개념이 아니라 그저 우리가 정보를 얻어가고 싶은 서버에 GET 요청으로 정보를 가져와서 우리가 그 정보를 가공해서 사용하는 것을 말합니다. 그래서 서버에 요청을 보낼 수 있게 request 모듈을 설치해야 합니다. 하지만 현재 우리가 typescript로 개발을 진행 중이기 때문에 @types/request모듈도 함께 설치해야합니다.

// C://projects/my-first-ts-express-croller
npm i request
npm i -D @types/request

그러면 crawl.ts를 만들어서 요청에 대한 응답이 잘 오는지 살펴봅시다.

// C://projects/my-first-ts-express-croller/src/crawl.ts
import request from 'request';
export const crawl = () => {
  request.get('https://naver.com', (err, res) => {
    if (err) console.log(err);
    console.log(res.body);
  });
};

이제 index.ts에서 crawl.ts의 동작을 확인해봅시다.

// C://projects/my-first-ts-express-croller/src/index.ts
import express from 'express';
import { crawl } from './crawl';
crawl();
const app = express();
app.get('/', (req, res) => res.send('hello'));

app.listen(8080, () => {
  console.log('server started!');
});

이제 이렇게 네이버에서 받아온 데이터를 더 쉽게 다룰 수 있는 형태로 만들어봅시다.

비동기처리하기

비동기처리에 대한 포스트는 추후에 작성하도록 하겠습니다. 먼저 비동기 프로그래밍을 잘 설명해둔 포스트들의 링크를 달아두겠습니다,

  1. 자바스크립트는-어떻게-작동하는가-이벤트-루프와-비동기-프로그래밍의-부상-async-await을-이용한-코딩-팁-다섯-가지
  2. 동기식 처리 모델 vs 비동기식 처리 모델

Callback

먼저, 비동기처리를 위한 가장 기초적인 방법인 콜백을 말씀드리겠습니다. 일반적으로 콜백함수는 이름 그대로 함수가 끝난 후에 호출될 함수를 말합니다. 자바스크립트에서는 함수가 일급 객체 즉, 매개변수로 사용될 수 있기 때문에 다음과 같이 매개변수에 끝나고 호출되길 원하는 함수를 전달해서 콜백함수를 사용합니다.

//예시
const myCallback = (a) => { console.log(a); };
const useCallback = (callback) => {
  //비동기로 동작할 예시코드 result = 10;
  const result = asyncFunc();
  callback(result);
}
useCallback(myCallback); // 10 출력

위 코드는 실제로 동작하진 않지만 콜백함수가 어떤 원리로 동작하는지는 이해할 수 있을 것입니다. 이제 콜백함수를 가지고 위에서 다뤘던 코드를 바꿔봅시다. 이번에는 /crawl 라우터로 접속하면 네이버 실시간 검색어를 보여주도록 바꿔보겠습니다.

// C://projects/my-first-ts-express-croller/src/crawl.ts
import request from 'request';
//express의 get 메서드의 타입정의
import { Send } from 'express';
export const crawl = (callback: Send) => {
  request.get('https://naver.com', (err, res) => {
    if (err) callback('');
    callback(res.body);
});
// C://projects/my-first-ts-express-croller/src/index.ts
import express from 'express';
import { crawl } from './crawl';

const app = express();
app.get('/crawl', (req, res) => {
  // crawl 내부에서 호출될 때 res.send 메서드의 this 값이 변경되는 것을 막기 위함
  crawl(res.send.bind(res));
});

app.listen(8080, () => {
  console.log('server started!');
});

브라우저에서 localhost:8080/crawl로 접속하면 다음과 같이 네이버 메인화면을 확인할 수 있습니다.

image.png

Promise

위 코드에서 다들 느끼셨을테지만 콜백은 depth(이어서 실행할 함수의 개수)가 조금만 깊어져도 코드를 작성하는 게 불편해지고, 가독성이 매우 떨어집니다. 여기서 나온 대비책이 바로 프로미스입니다. 프로미스는 비동기처리를 위해서 만들어진 객체라고 생각하시면 됩니다. 프로미스의 사용법은 아래와 같습니다.

// 예시
function getData() {
  return new Promise(function (resolve, reject) {
    const result = fetchData(function (response) {
      // 성공 시 resolve 호출
      resolve(response);
    });
    // 실패 시 reject 호출
    if(result.status === 'fail') reject('failed');
  });
}

getData()
  .then(res => {console.log(res)})
  .catch(err => {console.log(err)});

이제 이전에 콜백으로 작성한 코드를 프로미스를 사용한 코드로 바꿔봅시다.

// C://projects/my-first-ts-express-croller/src/crawl.ts
import request from 'request';
export const crawl = () =>
  // Promise 옆에 꺾쇠로 string을 감싸서 표현한 문법을 제네릭이라고 합니다.
  // 자세한 설명은 아래 링크를 참고해보세요.
  // https://ahnheejong.gitbook.io/ts-for-jsdev/03-basic-grammar/generics
  new Promise<string>((resolve, reject) => {
    request.get('https://naver.com', (err, res) => {
      if (err) reject(err);
      resolve(res.body);
    });
  });
// C://projects/my-first-ts-express-croller/src/index.ts
import express from 'express';
import { crawl } from './crawl';

const app = express();
app.get('/crawl', (req, res) => {
  crawl().then(result => res.send(result));
});

app.listen(8080, () => {
  console.log('server started!');
});

오히려 더 복잡해진 것 같긴 하지만, depth가 깊어졌을 때 가독성은 개선되었습니다. 하지만 더 개선해봅시다.

async, await

async, await은 ES2017에서 새로 나온 자바스크립트 문법입니다. 기존의 프로미스 코드를 실제로 실행할 때에는 then(), catch() 메서드를 통해서 실행해야 했지만, async, await 키워드만 붙이면 비동기 코드를 동기적으로 작성할 수 있게 됩니다.

위에서 작성한 예시 프로미스 코드를 async await코드로 바꿔보겠습니다.

// 예시
async function fetchData () {
  console.log(await getData1());
  console.log(await getData2());
  console.log(await getData3());
  console.log(await getData4());
}

위 코드를 보면 아시겠지만 async, await은 프로미스를 완전히 대체하는 개념이 아닙니다. 프로미스를 좀 더 효과적으로 사용할 수 있도록 도와주는 개념이라고 생각하시면 좋을 것 같습니다. 하지만 async 키워드를 사용하기 위해서 함수를 한 차례 감싸야한다는 조건이 필요합니다.

이제 실제 코드를 변환해봅시다. 위에서 말했듯이 async, await은 프로미스를 완전히 대체하는 것이 아니라 더 효율적으로 사용하기 위한 것이기 때문에 index.ts에서 프로미스를 사용하는 부분만 변경하면 됩니다.

// C://projects/my-first-ts-express-croller/src/index.ts
import express from 'express';

const app = express();
app.get('/crawl', async (req, res) => {
  const result = await crawl();
  res.send(result);
});

app.listen(8080, () => {
  console.log('server started!');
});

async, await으로 리팩토링한 코드 정말 짧고 간결하죠? 동기적으로 코드가 작성되어서 이해하기도 훨씬 쉽습니다.

지금까지 작성했던 코드는 모두 네이버 서버에서 받아온 데이터를 그대로 전달해오게 되는 코드였습니다. 이제는 이 HTML 데이터에서 실시간 검색어 목록을 추출해서 클라이언트에 전달해봅시다.

실시간 검색어 추출하기

우리가 크롤링에서 HTML 데이터에서 데이터를 추출할 때 사용할 라이브러리는 바로 cheerio입니다. cheeriojquery 문법과 css 선택자를 이용해서 HTML 내에서 자신이 필요한 정보를 가져올 수 있습니다.

// C://projects/my-first-ts-express-croller/src/extract.ts
import { load } from 'cheerio';

 export const extract = (html: string) => {
  if (html === '') return [];
  const $ = load(html);
  const crawledRealtimeKeywords = $(
    '.ah_roll_area.PM_CL_realtimeKeyword_rolling ul > li span.ah_k',
  );
  const keywords: string[] = $(crawledRealtimeKeywords)
    .map(
      (i, ele): string => {
        return $(ele).text();
      },
    )
    .get();
  return keywords;
};

위 코드가 네이버 페이지에서 실시간 검색어를 추출하는 코드입니다. 문자열 배열의 형태로 값을 리턴하게 됩니다. 이제 HTML 데이터에서 실시간 검색어만 추출해서 클라이언트에게 리턴해봅시다.

// C://projects/my-first-ts-express-croller/src/index.ts
import express from 'express';
import { crawl } from './crawl';
import { extract } from './extract';

const app = express();
app.get('/crawl', async (req, res) => {
  const result = await crawl();
  res.send(extract(result).join(', '));
});

app.listen(8080, () => {
  console.log('server started!');
});

이제 브라우저에서 /crawl로 접속하면 실시간 검색어가 보일 것 입니다. 다음 포스트에서는 데이터베이스에 실시간 검색어를 저장하는 기능을 구현해보겠습니다.

읽어주셔서 감사합니다.