퍼펫티어(Puppeteer)로 회사 경매에서 득템하기 💍

Jung Han
Jung-han
Published in
11 min readSep 1, 2019

시작하기 전에

19년 7월에 작성한 글입니다! 해당 기술에 관련된 내용은 매우 짧습니다. 일화에 가까운 내용입니다. 퍼펫티어가 무엇인지 궁금하신 분들은 ‘퍼펫티어(Puppeteer)는 무엇인가?’ 부터 보시면 됩니다.

왜 이런 짓을 시작했을까?

아버지가 인터넷, 문서 작업을 할 수 있는 저가 노트북을 찾고 계셨습니다. 적절한 가격대의 노트북을 구하던 중 회사 내에서 경매를 진행한다는 소식을 듣게 되었습니다. 20~30 정도로 맥북 에어를 구한다면 좋을 것 같았습니다.(양심도 없쥬?) 운영체제를 추가로 구매할 필요도 없고 맥북은 오래 돼도 많이 느려지지 않으니까요. 결정한 뒤 어떻게 하면 득템을 할 수 있을지 고민이 시작되었습니다.

사실 가장 큰 이유는 재밌는 글 주제가 될 것 같아서 시작 했습니다.

1부. 어떻게 득템할 수 있을까?

방법 1. 도움을 구한다.

감정에 호소한다.

이 방법은 동기들에게 권유되었지만 속지 않았습니다.

방법 2. 최저가에 마감 1분전 들어간다.

중고로운 평화나라에 형성된 가격

첫 번째 목표입니다. 목표로 하는 맥북은 중고가가 30–40 사이에 형성되어있었습니다. 회사에서 매일매일 사용되었을 테니 개인적으로 `20` 정도면 구매하기 적당한 것 같습니다.(양심이 없나요? 네.. 싸게 사고 싶었습니다 ㅠㅠ)

중고로운 평화나라에 형성된 가격(2)

두 번째 목표입니다. 맥북 에어 모델 중 가장 좋은 사양입니다. 19년 7월에 75만원에 거래된 내용이 있었습니다. 이 노트북이 `30`이 넘지 않는다면 들어갈 예정이었습니다.

그런데 여기서 문제가 발생합니다.

리스트에서 볼 수 있는 정보는 제목 뿐..

리스트에 나열된 정보는 `이름` 뿐입니다. 경매에 참여해 보신 분들은 아시겠지만 이름은 같지만, 실제 `사양`이나 `결함`이 달라집니다. 또한, 같은 사양임에도 글에 따라 가격이 천차만별이 됩니다.

마감 하루 전 저녁 10시 45분에 찍은 사진인데요. `Macbook Pro` 입니다. 입고 날짜, 사양 모든 게 같지만 4번 노트북만 댓글이 `19`개 입니다. 사진 기준으로 세 제품의 가격은 `220,-`원부터 `390,-`원입니다.

이런 이유가 발생하는 이유는 무엇일까요?

경매 룰

실물의 차이가 있다 해도 15만원이 차이 날 정도는 아니었을 겁니다. 추측건대 가장 큰 이유는 My Auction의 룰 때문이었을 겁니다. 한 번 노트북에 참여한 사람은 그 글에서 끝까지 참여할 수밖에 없습니다.(`이곳저곳 다니면서 작성하면 룰 위반이겠죠?`) 당연히 일찍 그 경매에 참여한다면 불리할 겁니다. 다른 싼 곳으로 가서 참여하면 되니까요.

낮은 가격에 적절한 노트북에 업어가는 방법은 제 생각엔 다음과 같았습니다.
마감이 얼마 남지 않았을 때 가장 낮은 금액을 가진 상품에 현재 금액 보다 약간 높은 금액으로 참여한다.

1분이 남았을 때 각 페이지에 직접 들어가지 않고 상품의 최고가를 비교하기 위해 저는 크롤링을 하게 됩니다. 그리고 그 크롤링을 위해 `퍼펫티어(puppeteer)`를 사용하게 됩니다.

2부. 퍼펫티어(Puppeteer)는 무엇인가?

퍼펫티어 로고

`퍼펫티어(Puppeteer)`의 뜻은 꼭두각시를 부리는 사람이란 뜻입니다. 로고를 보면 알 수 있듯이 나무 막대가 인형을 조종하듯 브라우저를 조종하는 것처럼 보입니다.

퍼펫티어는 Node 라이브러리로 구글에서 나온 Headless Chrome API입니다. Headless Browser는 간단하게 말해서 GUI가 없는 웹 브라우저를 의미합니다. 실제로 사용자는 브라우저를 조작하지 않지만, 코드를 통해 사이트에 접속하여 여러 가지 일을 수행할 수 있습니다. 소개 내용을 보면 웹 크롤링을 하거나 PDF 스크린샷을 찍을 때, E2E 테스팅을 할 때, 크롬익스텐션을 만들 때 등등의 일을 수행할 수 있다고 합니다. (* 참고: jest와 puppeteer를 함께 사용하여 테스트)

이 사이트에 접속해보면 웹 상에서 간단한 것들을 직접 수행해 볼 수 있습니다.

제가 퍼펫티어를 선택한 이유는 단순했습니다. 사용해 보고 싶었거든요. 혹시 크롤링이 필요한 때에는 퍼펫티어를 사용해 봐야겠다 생각했었습니다.

코드 훑어보기

필요한 정보 정하기

우선 계획을 만들어보면

  1. 크롤러를 통해 정보를 받는다.
  2. 적절한 타이밍에 링크를 통해 바로 접속한다.
  3. 가격 흐름에 맞는 적당한 가격을 작성한다!
  4. 경매에 승리한다.
계획이 다 있구나

완벽한 계획입니다. 필요한 정보를 정리해보면

  • 이름
  • 댓글 수(인기의 척도를 보려 했습니다..)
  • 링크(바로 이동해서 댓글을 작성하기 위해 필요 했습니다!)
  • 현재 가격

이정도만 있으면 득템을 하기에 충분한 정보인 것 같아 보입니다 .

퍼펫티어가 대신 해줘야 하는 일

퍼펫티어는 실행할 때마다 로그인을 해 페이지에 접속하고 My Auction 게시판에 들어가 아이템을 하나하나 클릭하는 행동을 대신 해줘야 합니다. 번호로 정리해보면

  1. 사내 사이트에 접속한다.
  2. 로그인을 한다.
  3. 게시판에 들어간다.
  4. 장비들을 순회하며 필요한 정보를 저장한다.
  5. 출력한다.

이 정도인 것 같습니다. 그럼 코드를 작성해보겠습니다.

코드 작성하기

작성된 코드를 간단하게 살펴 보겠습니다.

const puppeteer = require('puppeteer');

function crawler(id, password) {
// (1)
puppeteer.launch({
headless: true,
devtools: false
}).then(async browser => {
// 1. 사이트 접속
const page = await browser.newPage(); // (2)
await page.goto('http://google.com/'); // (3) 사이트 주소 입력

// 2. login
await page.type( "#user_id", id ); // (4)
await page.type( "#user_pw", password, {delay: 100});

const loginBtn = await page.$('.btn_login'); // (5)
await loginBtn.press('Enter');

//.... 나머지 작동
});

(1) `launch()`를 통해 퍼펫티어를 구동시킵니다. 이때 들어가는 옵션은 구동되는 크로미움 브라우저 옵션입니다. HTTPS에러를 무시하거나 headless 로 구동시킬지 등 여러가지 옵션을 지정할 수 있습니다.
(2) 구동이 완료된 뒤 반환된 브라우저에 `newPage()`를 통해 새 페이지를 만듭니다.
(3) 이후 `goto()`를 사용하여 매개변수로 넘어간 url로 페이지를 이동시킵니다.
(4) `type`은 첫번째 매개변수로 들어온 쿼리에 해당하는 엘리면트에 값을 타이핑 합니다. `delay`옵션을 주면 실제 사람이 입력하는 것처럼 천천히 입력하게 됩니다.
(5) `$`는 querySelector의 결과를 `$$`는 querySelectorAll의 결과를 반환합니다. 로그인 버튼을 찾아 로그인을 시도합니다.


// …로그인하기
await page.waitForXPath(‘//a[contains(text(), “My Auction”)]’); // (6)
const [myAuctionBtn] = await page.$x(“//a[contains(text(), ‘My Auction’)]”);
await myAuctionBtn.click();

// …나머지 작동
});

(6) `waitFor`를 통해 페이지에 My Auction이란 텍스트를 갖는 요소를 찾습니다. XPath를 통해 해당 텍스트를 가진 첫번째 요소를 찾습니다. 해당 요소를 찾으면 클릭해 리스트로 이동합니다.

 // … 
// (7)
const elem = await page.$(‘td.tit a’);
await elem.click();

// (8)
await page.waitFor(‘.view_tit .fl .tit’);
const title = await page.$eval(‘.view_tit .fl .tit’, (el) => el.innerText);
await page.waitFor(‘.post_info .fr a’);
const link = await page.$eval(‘.post_info .fr a’, (el) => el.href);
// … 정리 해서 출력

// (9)
await page.waitFor(‘.view_top .fl .btn_next’);
const nextPageBtn = await page.$(‘.view_top .fl .btn_next’);
if(nextPageBtn) {
await nextPageBtn.click();
await delay(1000);
}
});

(7) 첫번째 글을 찾아 클릭합니다.
(8) 그리고 제목과 링크 등을 정리하고
(9) 다음 글 버튼을 눌러 다음 글을 수집합니다.

하나의 시나리오를 작성하는 것처럼 퍼펫티어가 할 내용을 하나하나 작성해 줍니다. 브라우저가 매번 같은 시간에 로드 되는 것이 아니기 때문에 요소를 찾을 때나 이동할 때 `waitFor`를 작성하는 것을 잊지 말아야 합니다.

> 퍼펫티어의 API 문서는 상당히 읽기 쉽고 정리가 잘되어 있습니다. 더 궁금한 내용은 다음 링크에서 확인할 수 있습니다.

코드 작성이 끝났으면 `headless` 옵션을 `false` 로 두고 크롤러가 동작하는 모습을 간략하게 살펴 보겠습니다.

<사내 사이트여서..스크린샷을 올리지 못하는 점은 양해 부탁드립니다>

위 스크린샷은 제가 마우스로 조작하는 부분이 하나도 없습니다. 퍼펫티어는 사람이 직접 브라우저를 누르는것 처럼 하나하나 직접 방문해가면서 필요한 부분들을 뽑아냅니다.

결과를 터미널에 보여주는 스크린샷입니다. 이제 경매에 참여할 준비는 끝났습니다. 🤟

작성하면서 좋았던 점

첫번째로 노드 기반의 자바스크립트 코드로 작성할 수 있어 매우 좋았습니다. 퍼펫티어를 처음 사용해봤지만 코드를 모두 작성하는데는 한시간 정도 밖에 걸리지 않았던 것 같습니다. API 문서가 매우 잘 되어 있기 때문에 필요한 기능들을 찾기 편했고 그리 복잡한 내용을 작성한 것이 아니기 때문에 충분했습니다.

두번째로 사이트의 변경에 비교적 영향을 덜 받는다는 것 입니다.

아니 잘 되던 코드가 왜..

경매가 마감되기 전날 잘 작동하던 코드가 당일 점심부터 작동하지 않았습니다. 페이지를 이동할 때 에러가 발생했던 것입니다. 다음글을 넘어가는 버튼이 수정되었던 것이었고 데이터를 긁을 수 없었습니다. 만약 anchor태그의 href를 가져와 이동하는 방식으로 작성이 되었다면 꽤 큰 부분을 수정해야 했겠지만 페이지 이동시에 다음 페이지가 뜨는 delay를 조금 추가 해주는 방식으로 (당시는 많이 당황했지만) 쉽게 해결하였습니다.

3부. 득템에 성공했을까?

네. 실패했습니다.
6시 마감을 앞두고 5시 50분이 넘자 사내 사이트는 느려졌고 크롤러는 제 역할을 하지 못했습니다. 계속해서 크롤러를 돌린다면 사이트에 부하가 될 것 같아 직접 참여하기로 마음먹었습니다. 하지만 이미 모든 장비들은 예상보다 훨씬 비싼 가격대가 형성되었고 저는 돈이 부족하여 포기하였습니다.(🙄) 계획은 완벽했지만 너무 적은 돈을 들고 나왔었던것이 패인이였습니다.

본인 3만 4천원에 모니터사는 상상함ㅋㅋ

(모니터라도 하나 장만해 보려 했지만 30초 차이로 실패했습니다.)

경매에서 승리하려면 큰 돈을 부르는 것이 가장 좋은 전략인 것을 알게 된 좋은 경험이였습니다.

혹시 이후에 크롤링이 필요하거나 다른 재밌는 일을 기획한다면 퍼펫티어는 하나의 좋은 선택지가 될 수 있을 것 같습니다. 서론이 60인 가벼운 글이었는데 읽어주셔서 감사하고 재밌는 경매를 기획해주신 많은 분들 감사합니다.🙏

처음 퍼펫티어를 사용했기 때문에 많이 부족한 글일 수 있습니다. 잘못된 내용이 있다면 댓글로 남겨주시면 감사하겠습니다!
코드는 다음 깃헙에서 확인할 수 있습니다.
👉 https://github.com/jung-han/Auction_puppeteer

--

--

Jung Han
Jung-han

개인용 블로그로 사용하고 있습니다. 좋은 개발자가 꿈입니다. > https://www.notion.so/Han-Jung-c43f4bcd2b3f4b3d85b93aee41c5e098