일곱번째 회고록을 작성하게 되었다. 어느 덧 2월의 마지막날에 성큼 가까워졌다. 이 주의 끝자락에는 새로 6주를 같이 진행할 조원이 꾸려졌고, 실전 프로젝트를 진행하게 된다. 아무쪼록 좋은 결과물을 얻어낼 수 있으면 좋겠다.
이번에 클론코딩 프로젝트로 작업하게 된 것은 에어비앤비였다. 2017년에 호주 여행때 써본 뒤로는 사실 코로나-19의 영향으로 써본 기억이 없었지만, 어떤 앱인지는 알고 있었다. 백엔드 기준의 구현 범위는 정말 어느정도 레벨의 기능까지 추가할 지에 따라 너무나 달라질 수 있어서 클론코딩을 어느 범위까지 할 지 스코프를 명확히 해야했다.
구현 범위를 아래와 같이 잡았다.
이번 프로젝트도, 마찬가지로 프로젝트 기간이 사실상 6일밖에 주어지지 않았기 때문에, 와이어프레임을 그리는 것은 tryeraser를 그냥 사용했다. ER Diagram 을 그리는 것은 drawsql.app 을 사용했다.
이전에 한 기억이 있어서, 와이어프레임과 API 설계는 반나절만에 거의 완성했다. 아무래도, 서비스가 명확하고 특정 기능을 대상으로 하는 것이다 보니 이전보단 금방 끝났다.
클론코딩의 의의는 아무래도 이미 만들어진 서비스를 보며, 이것을 구현하려면 어떤 작업들이 필요하고 데이터를 어떻게 구성해야 될지를 고민하는 것이라고 생각이 들었다. 그러면서도, 보여질 데이터들을 잘 쌓아주어야 이쁘게 나올것이라는 생각이 들어 에어비앤비 페이지를 스크래핑 시도해봤다.
우선, 본래라면 메인페이지에 해당하는 유연한 검색 페이지에서 각 숙소의 URL 정보를 쭉 스크랩한 후에, 개별적으로 돌면서 상세 내용을 긁어오려고 했는데 시간내에 어려울 수 있어서 우선 보류했다.
스크래핑 시도한 메인페이지 (결국 포기)
숙소 URL 목록을 배열로 저장하고 그것을 순차적으로 긁어오도록 만들었다.
아래는 시작한 코드이다.
const puppeteer = require('puppeteer');
const crawler = async() => {
try {
const browser = await puppeteer.launch({
headless: false
});
// 새로운 페이지를 연다.
const page = await browser.newPage();
// 페이지의 크기를 설정한다.
await page.setViewport({
width: 1920,
height: 1080
});
let urlString = "https://www.airbnb.co.kr/rooms/33816652?category_tag=Tag%3A5348&adults=1&children=0&infants=0&check_in=2022-08-08&check_out=2022-08-15&federated_search_id=0b5c0aec-4f5d-4e7b-b194-8350ef0bb2da&source_impression_id=p3_1645435331_J48WQMlw6Ff%2FX0lq"
let urlObject = url.parse(urlString, true); // url 주소내에 파싱할 것이 있을 경우, urlString을 Object 형태로 변환해준다.
await page.goto(urlString);
await page.waitForTimeout(10000); // 페이지 로딩이 되기까지 잠시 기다린다. 테스트 중인 기기의 사양이나 인터넷 속도, 웹서버의 속도 따라 경험적으로 테스트해야함.
// 숙소 호스팅 유저 이름
let host_name = await page.$eval(
"#site-content > div > div:nth-child(1) > div:nth-child(6) > div > div > div > div:nth-child(2) > section > div.c6y5den.dir.dir-ltr > div.tehcqxo.dir.dir-ltr > h2", element => {
return element.textContent;
}); // 원하는 html 태그를 copy selector 로 가져온 후, textContent만 추출한다.
console.log(host_name) // Andrew님
...
await page.waitForTimeout(3000);
await browser.close();
} catch (e) {
console.error(e);
}
crawler();
에어비앤비의 숙소 정보 페이지에서 보이는 주소가 상당히 인상적이었는데, 정말 온갖 요소가 다 들어있었다. Node.js의 기본 라이브러리 기능 중 하나인 url.parse()
를 써보니, 쿼리를 분석할 수 있었다.
console.log(urlObject.query);
//urlObject.query
[Object: null prototype] {
category_tag: 'Tag:5348', // '통나무집' 태그
adults: '1', // 성인 1명
children: '0', // 어린이 1명
infants: '0', // 유아 1명
check_in: '2022-08-08', // 체크인 날짜
check_out: '2022-08-15', // 체크아웃 날짜
federated_search_id: '0b5c0aec-4f5d-4e7b-b194-8350ef0bb2da', // 불명
source_impression_id: 'p3_1645435331_J48WQMlw6Ff/X0lq' // 불명
}
이런식으로 어떤 정보를 입력했는지 정말 상세하게 나누어둔 것을 확인했다. 요소들을 선택할 때는, 크롬 개발자도구의 현재 페이지 Elements를 선택하고 Copy Selector
를 사용하여 요소들을 탐색했다. div class 명들이 난독화 되어있어 내심 걱정했는데, 다행히 유저별 난독화가 아니었다.
이후에는 다소 노가다 작업이다.
// 숙소 이름
let home_name = await page.$eval(
"#site-content > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div > div > div > div > section > div._b8stb0 > span > h1", element => {
return element.textContent;
}); // 원하는 html 태그를 copy selector 로 가져온 후, textContent만 추출한다.w
console.log(home_name);
위와같이 탐색할 태그를 찾아서 지정해주고, page.$eval
통해 탐색한 후 element.textContext
로 찾아낸다. 이후에는 모아서 데이터베이스에 저장해주면 된다.
이와 같은 과정을 통해 프론트엔드 분들이 필요한 정보를 모아서 전달해주었다. 아래와 같은 내용을 넘겼다.
const homeInfo = {
"home_name": home_name, // 숙소 이름
"host_name": host_name, // 호스트 이름
"category": category, // 집 종류
"rateAvg": rateAvg, // 별 평점
"comment_count":parseInt(comment_count), // 후기 갯수
"address": address, // 주소
"latitude": parseFloat(latitude), // 위도
"longitude": parseFloat(longitude), // 경도
"image_url": image_url, // 이미지URL 목록 []
"introduce": introduce, // 소개글
"price": price, // 가격
"convenience": convenience, // 편의시설 목록 []
"availableDate": availableDate // 숙박 가능 날짜.
}
이번 작업에선 사실 서버 코드를 작성한 것들이 크게 없었다. 초기에 스키마를 구성하는 부분에서 좀 신경써서 진행했다. 찜하기 테이블
을 별도로 만들고, 그에 해당하는 값을 내려보내주는 부분의 소스코드만 중간중간 이어서 작성했다.
그 외에는 새벽 3시까지 남아있다가 남아있는 프론트엔드 분들에게 이것도 안되고 저것도 안된다고 하면 일부 덜 작성된 코드가 있거나 버그가 있는것을 수정해주는 작업을 위주로 했다. 기존 코드를 분석하고 어떤 부분이 잘못 되었는지 확인하고 기록을 남겨두고, 임시로 수정해서 되는지 테스트 해본 후에, 다음 날 같이 작업했던 백엔드 분들에게 전달하여 수정을 같이 진행했다.
다행히 무탈하게 잘 마무리되고, 지도부분이 잘 연동이 안되서 아쉽게 해당 데이터를 사용하진 못했다.
이번에도 역시 MySQL과 Sequelize 를 써봤으면 좋을텐데, 제대로 건드려보지 못했다. 실전 프로젝트때는 써보게 될텐데, 아마 이때 그동안 안해봤던 기술 부채를 경험하지 않을까 한다.
새로이 6주짜리 실전프로젝트가 시작되었다. 창작의 고통을 고스란히 느끼고 있는데, 기존 서비스를 둘러보며 차별점을 둘 수 있는 것들을 찾아 헤매다 괜찮은 조합으로 될 거같은 것을 만나 기획에서 개발로 잘 발전시키고 있다. 다행히 같은 조원들의 평도 괜찮고, 실제로도 초기 아이디어 평가가 괜찮게 나와서 8주차때 속도를 내서 개발해볼 수 있을 것으로 보인다.
리더 역할도 맡게되어 부담은 다소 있지만, 내가 이전 대학생활 때부터 잘 해왔던 것처럼 하면 잘 이끌어갈 수 있을거라 생각한다. 여전히 회의를 주도적으로 진행하는 것은 부담스럽긴 하지만 할 수 있다. 과거보다 체력이 많이 떨어져서 그 부분만 잘 관리해주면, 아주 잘 끝낼거 같아 이번 6주 일정이 기대된다. 많은 것을 배우고 경험해나갔으면 좋겠다.
Node.js | URL parsing - 요청에 응답하기
[nodejs 크롤링] 2장. puppeteer 크롤링
Puppeteer를 이용한 웹 크롤링 해보기 (예제 1)