Node.js용으로 하루 정도 시간 내서 만들어봤던 프로그램이다.


전체 소스 코드는 https://github.com/WKBae/Node-Naver-Webtoon-Downloader에 있고, 클론해서 npm install을 실행해서 의존 파일들을 받아야 실행된다.


사용법은 "node download.js <네이버 웹툰 ID>"로, 예를 들어 조석 작가님의 마음의소리는 웹툰 주소가 "http://comic.naver.com/webtoon/list.nhn?titleId=20853&weekday=tue"이고, 다운받기 위해서는 "node download.js 20853"라고 하면 된다. (회차 범위 지정은 아직 안넣었으니 저 많은 회차를 전부 다운로드하지는 말기를.. 약 300회 가량 업로드된 웹툰은 1GB정도의 공간을 차지하니 남은 공간도 잘 봐야 한다.)


그리고 웹툰을 다운로드 받고자 한다면 무선랜보다는 유선랜에 연결된 상태에서 하는 것이 좋다. 회차 당 여러 이미지들, 여러 회차(최대 10회까지)를 동시에 받으니 인터넷이 좋지 않다면 네트워크 오류를 뿜으며 종료된다. 다운로드 받는 동안은 인터넷이 매우 느려지니 공용 네트워크에서도 사용하지 말자.


이 프로그램은 기능 테스트이며, 개인적인 사용에 한합니다. 다운로드 받은 웹툰의 배포, 웹툰과 프로그램의 영리적인 사용은 엄격히 금지합니다.



Node.js가 지원하는 자바스크립트의 새 기능들을 많이 써보기도 했고, 기타 라이브러리나 HTML 파싱, HTTP 쿠키 관련해서도 많이 건드려봤다.

특별히 쓰인 코드들은 다음과 같다.


1. 람다 식


특별히 어디랄 데 없이 광범위하게 사용했다. 콜백 함수를 넘겨줄 때, 이름을 굳이 지어주지 않아도 될 함수들은 전부 람다 식으로 작성했다.


2. async.js 함수들


주로 콜백으로 이어지는 자바스크립트 I/O 특성상, 이런 툴을 쓰니 이어지는 작업이 더 간단해졌다.

특히 주 작업 함수에서 사용한 async.waterfall() 함수는 순차적인 작업을 수행할 때 콜백 지옥을 피할 수 있게 해줬으며, 다운로드 작업 시 사용한 async.parallel()과 async.parallelLimit() 함수로 동시 다운로드가 되게 해서 속도를 높일 수 있었다.


3. Generator function, Spread operator


download.js 60번째 줄

function* webtoonTaskGenerator(titleId, last) { ... for(let i = 1; i <= last; i++) { yield function webtoonGetter(callback) { ...

download.js 390번째 줄

async.parallelLimit([...webtoonTaskGenerator(titleId, toon.last)], 10, (err, results) => {

download.js 170번째 줄

function* imageTaskGenerator(items, referer, dir) { for(let i = 0; i < items.length; i++) { let elem = items[i]; yield (callback) => { ...

download.js 116번째 줄

async.parallel([...imageTaskGenerator($images, url, path.resolve(OUTPUT_DIR, titleId, i+""))], callback);

async,js에게 넘겨줄 작업을 생성하는 함수이다. async.js에서도 iterable을 지원하는데, 내가 쓸 땐 뭔가 문제가 생겨서 spread operator을 쓴 것으로 기억한다.


4. 네이버 로그인 세션 쿠키 입력


download.js 129번째 줄

} else if(res.statusCode == 302) { if(!doingAuthInput) { doingAuthInput = true; console.log("Failed to load toon#" + i + ", trying with user auth info can help."); rl.question("NID_AUT: ", (aut) => { rl.question("NID_SES: ", (ses) => { jar.setCookie(request.cookie("NID_AUT=" + aut), "http://comic.naver.com"); jar.setCookie(request.cookie("NID_SES=" + ses), "http://comic.naver.com"); doingAuthInput = false; webtoonGetter(callback); // Retry with session information }) }); } else { //console.log("Failed to load toon#" + i + ". Waiting until input is done..."); (function waitInputDone() { if(doingAuthInput) { async.setImmediate(waitInputDone); } else { webtoonGetter(callback); } })(); } }

이게 가장 최근에 추가했던 기능이다.


성인 웹툰은 사용자 로그인이 필요하다. 로그인되지 않은 상태에서 성인 웹툰 보기 페이지에 들어가면 로그인 페이지로 302 리디렉션을 넘겨주는데, 로그인을 직접 구현하기에는 귀찮고 힘들다.(난 OTP까지 걸어놔서 특히!)

그런 고로 사용자에게 현재 로그인된 세션의 쿠키를 입력하도록 질의한다.


해본 결과 생각보다 간단하게 해결되서 놀랐다. 이렇게 금방 될 줄은 몰랐는데, 아무튼 성공적으로 로그인 인증이 되어서 성인 웹툰도 다운로드받을 수 있다.


이 프로그램은 여러 회차를 동시에 다운로드 받으므로 여러 회차가 동시에 302 리디렉션 응답을 받을 것이다. 동시에 여러 개의 질문에 응답할 수는 없고, node.js의 readline도 꼬이므로 doingAuthInput 변수에 지금 이미 쿠키 입력을 받고 있는지를 저장한다.


입력을 받고있지 않으면 쿠키 정보를 입력받고, 이미 입력을 받고있다면 아래의 else문을 실행한다.

else문에서는 waitInputDone() 함수가 있는데, 여기서는 입력을 받을때까지 setImmediate() 함수를 이용해서 다른 동시작업이 먼저 실행되도록 자기 자신을 미뤄서, 입력을 받는 부분이 먼저 실행되어 입력을 완료하도록 대기한다.

그러다가 입력이 완료되면 다운로드를 재시작(자기 자신의 함수를 다시 호출 - webtoonGetter(callback))한다.


이 병렬 처리 과정도 일반적인 자바스크립트 코딩을 하면 볼 일이 없겠지만 어쩌다보니 쓰게 되어서 신기했다. 타 언어의 스레딩과는 달리 데이터 완전성 이런건 확인을 안해도 되어서 좋았지만, synchronized block 같은 기능을 생각하고 저 부분을 짜다가 나중에 보니 비슷한 기능은 없고 setImmeriate() 함수를 찾게 되어 그걸 사용하게 되었다.

블로그 이미지

예비컴공돌이

각종 프로젝트 진행중! 생각날때마다 블로그 업데이트합니다.