Node.js와 헤드리스 크롬으로 크롤링을 챗봇에 적용해보자

안녕하세요? LINE에서 Developer Advocate를 맡고 있는 Nakajima입니다.

챗봇으로 웹을 스마트하게 사용하기

여러분, 혹시 다음과 같은 경험 없으셨나요?

  • 스마트폰이 지원되지 않는 웹사이트에서 뭔가 입력할 때면 짜증이 난다.
  • 매번 웹사이트에 로그인하기가 번거롭다.
  • 애초에 스마트폰으로는 입력란에 입력하는 것 자체가 힘들다.

당연한 얘기지만 LINE 앱은 주로 스마트폰으로 이용합니다. 그래서 LINE은 스마트폰 UX를 중요하게 여기고 많은 노력을 기울이고 있습니다. 이에, 앞서 언급한 사례들을 LINE으로 쉽게 이용할 수 있다면 편리하지 않을까라는 생각에서 시작된 한 가지 실험을 본 포스팅에서 소개할까 합니다.

챗봇으로 웹 페이지를 이용할 때는 헤드리스 크롬으로

웹 브라우저 중 하나인 Chrome은 다들 아실텐데요, 혹시 헤드리스 크롬(Headless Chrome)도 알고 계시나요? 일반적으로 Chrome을 GUI(Graphic User Interface) 형태로 실행하는 것과는 달리, 헤드리스 크롬은 CLI(Command Line Interface) 형태로 실행하는 것을 말합니다. 눈에 보이는 UI가 필요 없는 테스트 자동화 등에 많이 쓰이는데요, 챗봇으로 웹을 실행할 때도 역시 눈으로 확인하여 수행하는 작업이 필요가 없으니 안성맞춤입니다.

챗봇이 도서관 웹사이트에 접속하여 책을 예약, 대출하는 아래 동영상을 한번 보시겠습니다.

이 동영상에서는 웹사이트가 작동하는 모습을 우리가 볼 수 있도록 크롬을 헤드리스 모드로 실행하지 않고 일반적인 브라우저의 형태로 보여주고 있습니다. 실제로는 UI 없이 헤드리스 모드로 실행합니다.

챗봇과의 대화가 진행됨에 따라 웹사이트가 작동하는 모습을 확인할 수 있습니다. 어떤 책을 예약해 달라고 챗봇에게 말만 하면 이후 웹사이트에서 수행되어야 하는 동작은 챗봇이 알아서 처리해 줍니다. 이런 식으로 여러 가지 절차가 필요한 웹 조작, 그 중에서도 반복적인 작업을 챗봇에게 맡기면 절차를 줄일 수 있고, 결과적으로 UX에 좋은 변화를 줄 수 있습니다.

구현 방법

앞서 영상에서 보았던 기능에 대한 기본적인 구현 부분을 소개해 드리겠습니다. 우선 챗봇의 본체는 Node.js 기반으로 만듭니다. Node.js에는 puppeteer라는 NPM 패키지가 있는데요, 헤드리스 크롬을 쉽게 이용할 수 있게 해줍니다. 한 번 설치해보겠습니다.

설치하기

다음의 명령어를 입력하여 puppeteer를 설치하세요.

$ npm install --save puppeteer

코딩하기

먼저 브라우저의 인스턴스를 실행합니다. 브라우저 인스턴스는 실행할 때 시간도 오래 걸리고 메모리도 많이 사용합니다. 다양한 유스케이스를 고려한다면 서버를 구동시킬 때 브라우저 인스턴스를 실행시켜 두는 편이 좋습니다.

const pptr = require("puppeteer");
(async () => { // 브라우저 인스턴스 실행하기 let browser = await pptr.launch({ headless: true }); })();

다음으로 브라우징 콘텍스트를 생성합니다. 이는 쿠키 등 개별 사용자의 정보를 독립시키기 위한 실행 공간이 됩니다.

// 브라우징 콘텍스트 생성하기(사용자별로 독립된 공간)
let bc = await browser.createIncognitoBrowserContext();

다음으로는 페이지를 생성합니다. 다음 부분은 탭에 해당하는 페이지입니다.

// 페이지 생성하기(탭에 해당)
let page = await bc.newPage();

이제 헤드리스 크롬의 인스턴스는 어떤 URL에 접속해서 응답을 반환하거나 form 요소가 있는 웹페이지라면 form 요소를 사용할 수 있는 상태가 됩니다.

// 임의의 URL에 접속하기
await page.goto(임의의 URL, {
    waitUntil: "networkidle0"
});

웹페이지에 입력 필드가 있다면, 값을 입력하기 위해 ‘type()’ 메서드를 이용합니다.

await page.type(CSS 선택자, 'foo');

버튼 요소가 있다면, ‘click()’ 메서드를 이용해 버튼을 클릭하는 동작을 수행할 수 있습니다.

await page.click(CSS 선택자);

자, 이제 다음과 같은 표가 있다고 생각해 봅시다. 이 표의 ‘제목’ 행의 값으로 목록을 만들어 보겠습니다.

번호 제목 저자
1 불꽃(火花) 마타요시(又吉)
2 일본재흥전략(日本再興戦略) 오치아이 요이치(落合陽一)

다음의 코드로 표에 있는 데이터를 가져와서 ‘제목’ 행의 값만 추려내어 목록을 만듭니다.

// 임의의 표의 행을 모두 가져오기
let rows = await page.$$('table > tr');
let title_list = [];
for (let row of rows){
    let cols = await row.$$('td');
    title_list.push(await (await cols[1].getProperty("textContent")).jsonValue());
}
// title_list에 제목 목록이 string 배열로 들어가 있음
console.log(title_list);

위의 코드로 ‘title_list’배열에 제목 목록이 저장됩니다. 남은 일은 Flex Message를 사용해서 사용자가 보기 좋게 표현하는 일뿐이지요. 이 일련의 과정은 즉, 사용자가 챗봇에게 하는 말 가운데 웹상에서 처리하는 데 필요한 부분을 선택하고 수행한 뒤 그 결과를 사용자에게 제공하는 방식으로 LINE과 웹의 상호작용을 구현한 과정입니다.

유의할 점

우리가 다룬 작업에 대해 유의할 점 몇 가지를 나누고자 합니다.

  • HTML 구조에 의존적입니다. 따라서 웹사이트의 HTML이 변경된 경우, 특히 버튼이나 입력 필드를 보충하기 위한 CSS 선택자를 수정해야 합니다.
  • 메모리를 많이 사용합니다. 사용자 한 명 당 최소 20MB 정도는 필요할 수 있기 때문에 접속이 대량 발생하는 서비스에는 맞지 않습니다.
  • 챗봇이 갖고 있는 콘텍스트 정보(지금까지의 대화 내용, 이제부터 사용자에게 물어봐야 하는 내용 등)와 브라우저의 상태를 일치시켜야 하기 때문에 코딩의 난이도가 높은 편입니다. 대화가 예상했던 시나리오 대로 흘러가면 문제가 없지만, 가령 웹에서 몇 페이지 앞의 입력 항목을 수정해야 하는 등 고려되지 않은 유스케이스는 대응하기 어렵습니다.

따라서 누구에게나 추천하거나 범용적으로 적용하기는 어렵겠지만, 지금까지는 불가능해 보였던 챗봇과 웹의 접목을 시도해 볼 수 있다는 점에서 알아두면 좋은 기술이 아닐까 생각합니다.

다음은 인공지능 스피커?

이번에는 챗봇과 웹페이지를 연결해 보았는데요, 더 나아가 인공지능 스피커와 웹사이트를 연결하는 것도 충분히 가능하겠지요. 지금까지는 웹이라고 하면 ‘보는 것’이었는데, 이제 ‘음성으로 대화하기’라는 새로운 UX가 실현될지도 모르겠습니다. 여러분, 한번 도전해 보시겠어요?

Related Post