Outsider's Dev Story: JavaScript/Library 카테고리 글 목록https://blog.outsider.ne.kr/Stay Hungry. Stay Foolish. Don't Be Satisfied.2024-03-15T12:55:06+09:00Textcube 1.10.7 : Tempo primo2captcha를 이용해서 사이트의 CAPTCHA 자동화로 처리하기Outsiderhttps://blog.outsider.ne.kr/15472021-04-26T22:09:53+09:002021-04-18T03:37:15+09:00<p><strong>이 글은 <a href="https://2captcha.com/?utm_medium=content&utm_source=outsider&utm_campaign=korea">2Captcha</a>로부터 비용을 받고 작성하는 글이다.</strong></p>
<p>광고성 글은 거의 안 쓰는 편이라 비용을 받고 블로그에 글을 쓰는 건 처음인데 서비스 자체가 종종 필요한 경우가 있어서 도움이 될 부분이 있어 보였기에 작성하기로 했다. 글을 받아서 올리는 건 아니고 2capthca에 대한 소개 글을 써달라는 요청만 받고 글의 구성이나 예제는 모두 직접 작성했다.<br />
<br></p>
<h1>CAPTCHA</h1>
<p>발음도 어려운 CAPTCHA는 "<strong>C</strong>ompletely <strong>A</strong>utomated <strong>P</strong>ublic <strong>T</strong>uring test to tell <strong>C</strong>omputers and <strong>H</strong>umans <strong>A</strong>part"의 약자로 사람과 컴퓨터를 구분하는 완전 자동화된 튜링 테스트를 말한다.</p>
<p>보통 로그인이나 회원 가입 같은 사용자 액션에 보통 봇이라고 부르는 자동화 프로그램을 막기 위해서 CAPTCHA를 넣고 일반적으로 컴퓨터는 읽기 어려운 문자를 보여주고 사람이 입력하게 한다.(때로는 사람도 읽기가 어렵다) 최근에는 다양한 CAPTCHA가 개발되어 여러 이미지 중에 선택하거나 비슷한 이미지를 고르거나 이미지를 회전시키는 등의 방법도 사용되고 있다.</p>
<p>CAPTCHA를 이용해서 자동화된 봇을 막는 이유도 있고 이를 뚫으려는 이유도 있다. 남의 사이트를 악의적으로 조작하는 것은 아니라고 하더라도 CAPTCHA 때문에 자동화를 못 한 경험이 쉽게 할 수 있다. 나 같은 경우도 사용하는 서비스에서 보고서 관련 API를 제공하지 않아서 매달 혹은 매주 사람이 수동으로 데이터를 정리해야 했기에 이를 자동화해보려고 했지만, CAPTCHA에 막혀서 제대로 하지 못한 기억이 있다.<br />
<br></p>
<h1><a href="https://2captcha.com/?utm_medium=content&utm_source=outsider&utm_campaign=korea">2Captcha</a></h1>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/3777893615.jpg" width="750" height="404" alt="2Captcha 홈페이지" title="" /></p>
<p><a href="https://2captcha.com/?utm_medium=content&utm_source=outsider&utm_campaign=korea">2Captcha</a>는 이러한 Captcha를 해결해주는 유료 서비스다.</p>
<blockquote>
<p>2Captcha's main purpose is solving your CAPTCHAs in a quick and accurate way by human employees, but the service is not limited only to CAPTCHA solving.</p>
</blockquote>
<p><a href="https://2captcha.com/2captcha-api?utm_medium=content&utm_source=outsider&utm_campaign=korea">문서</a>를 보면 CAPTCHA를 사람이 해결해 주는 것으로 보인다. CAPTCHA 해결을 요청하면 사람이 이를 보고 직접 해결하고 그 처리된 값을 나한테 다시 알려주면 이를 이용해서 CAPTCHA를 해결할 수 있는 것이다. reCaptcha, hCaptcha, KeyCaptcha, RotateCaptcha등 다양한 CAPTCHA를 지원하고 여러 언어의 SDK도 제공하지만, HTTP API를 제공하므로 어떤 언어에서든 사용할 수 있다.</p>
<p>다음과 같은 과정으로 진행된다.</p>
<ol>
<li>해결할 CAPTCHA를 2captcha에 요청하면 응답으로 해당 요청의 ID를 알려준다.</li>
<li>이 요청 ID로 해당 작업이 완료되었는지 반복해서 확인한다.</li>
<li>20~50초 후 요청이 완료되면 응답 값을 알려주고 이 값을 이용해서 CAPTHCA를 풀 수 있다.</li>
</ol>
<p>그리고 2Captcha는 유료서비스이다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/6588251213.jpg" width="750" height="519" alt="2Captcha의 CAPTCHA별 가격표" title="" /></p>
<p>CAPTCHA 종류에 따라 다른데 <a href="https://2captcha.com/2captcha-api?utm_medium=content&utm_source=outsider&utm_campaign=korea#rates">가격표</a>를 보면 많이 쓰는 reCaptcha가 1,000번 요청에 2.99달러이므로 크게 부담될 가격은 아니라고 생각한다.<br />
<br></p>
<h1>Walmart 로그인 데모</h1>
<p>Walmart는 로그인 시에 <a href="https://developers.google.com/recaptcha/docs/display">reCaptcha v2</a>를 사용하고 있다. 사실 서비스마다 CAPTCHA를 사용하는 패턴이 다르기에 적당한 예제를 찾는데도 꽤 오래 걸렸다. 많은 사이트가 CAPTHCA를 항상 보여주는 게 아니라(그런 사이트도 있지만...) 여러 조건을 검사하고 보여줬다 안 보여줬다 하고 봇을 막으로면 CAPTCHA에만 의존하는 것이 아니라 추가적인 방법도 함께 사용하기 때문에(폼의 DOM 변형을 준다거나 유저 에이전트로 차단한다거나...) 실제로 자동화를 하려면 이러한 부분도 다 찾아서 해결해야 한다.</p>
<p>여기서는 2captcha로 어떻게 reCaptcha v2를 해결하는지를 보여줄 것이고 실제로 사용하려면 대상 사이트의 패턴을 알아야 자동화를 할 수 있을 것이다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/6072456334.jpg" width="500" height="764" alt="월마트 로그인 페이지" title="" /></p>
<p><a href="https://www.walmart.com/account/login">Walmart의 로그인 페이지</a>는 CAPTCHA를 요구한다. 주로 사용하는 브라우저에서는 보통 자동으로 넘어가지만, 시크릿 모드로 접속하면 다음과 같이 reCaptcha v2가 나타난다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/7595094470.jpg" width="500" height="548" alt="로그인 했을 때 나오는 reCapthca 화면으로 I'm not a robot를 확인하라고 나온다" title="" /></p>
<p>"I'm not a robot"을 입력하면 보통 많이 보았을 여러 사진 중 버스를 고르라거나 신호등을 고르라거나 해서 이미지 경계에 살짝 버스가 걸리면 이 이미지도 선택해야 하나 말아야 하나 고민하게 만드는 과정이 진행된다.<br />
<br></p>
<h2>로그인 자동화</h2>
<p>단순히 CAPTCHA만 푸는 것이 아니라 로그인 자체를 완전히 자동화하기 위해 <a href="https://playwright.dev/">Playwright</a>를 사용했다. Playwright는 브라우저를 조작하는 API를 사용해서 자동화할 수 있게 하는 프로젝트고 Microsoft에서 만들었다. 주로 <a href="https://pptr.dev/">Puppeteer</a>를 사용했지만 궁금해서 이번에는 Playwright를 사용했다. 전체 예제는 <a href="https://github.com/outsideris/2captcha-example">GitHub에 올려두었다</a>.</p>
<pre class="line-numbers"><code class="language-js">const playwright = require('playwright');
const got = require('got');
const ID = process.env.ID;
const PASSWORD = process.env.PASSWORD;
</code></pre>
<p><code>playwright</code>를 사용했고 HTTP 라이브러리로 <a href="https://www.npmjs.com/package/got"><code>got</code></a>을 사용했다. 월마트에 로그인할 계정정보가 필요하므로 환경변수에서 가져오도록 했다.</p>
<pre class="line-numbers"><code class="language-js">const browser = await playwright.chromium.launch({});
const context = await browser.newContext();
const page = await context.newPage();
// login page
await page.goto('https://www.walmart.com/account/login');
</code></pre>
<p><code>playwright</code>를 사용해서 크롬 브라우저를 실행하고 월마트 로그인 페이지에 접속했다. 이 코드로는 헤드레스 크롬이 실행되고 실제 크롬으로 실행하려면 <code>playwright.chromium.launch({ headless: false })</code>처럼 <a href="https://playwright.dev/docs/api/class-browsertype?_highlight=launch#browsertypelaunchoptions">옵션</a>을 주어야 한다.</p>
<pre class="line-numbers"><code class="language-js">// enter email
await page.type('#email', ID, {delay: 100});
await page.screenshot({ path: 'img/id.png' });
// check password field in a form
const hasPassword = await page.$('#sign-in-form');
if (hasPassword) {
console.log('has password field')
// enter password
await page.type('#password', PASSWORD, {delay: 100});
} else {
console.log('no password field')
await page.click('#sign-in-with-email-validation [type=submit]');
// enter password
await page.type('#sign-in-password-no-otp', PASSWORD, {delay: 100});
// login
await page.click('#sign-in-with-password-form [type=submit]');
}
// login
await page.click('#sign-in-form [type=submit]');
</code></pre>
<p>로그인 페이지에서 이메일과 비밀번호를 입력하고 로그인 버튼을 누르는 코드이다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/7881752395.jpg" width="354" height="537" alt="playwright로 이메일, 비밀번호를 입력한 화면" title="" /></p>
<p>이메일과 비밀번호가 잘 입력되는 것을 확인할 수 있다. 위 코드에서 <code>if (hasPassword) {}</code> 조건절이 있는 이유는 가끔 위 스크린숏과 달리 이메일만 입력하고 로그인 버튼을 누르면 패스워드 입력란이 나오는 UI가 등장한다. 이 부분도 아마 자동화를 막기 위해 변형을 준 것 같아서 처리를 했는데 테스트했을 때는 <code>else</code> 조건으로 빠지는 UI는 거의 등장하지 않아서 사실 이 부분은 제대로 처리하지 않았다.(동작하지 않는다는 의미이다) 여기 예제에서는 월마트의 구현이 중요한 건 아니라서 상관없다.</p>
<h2>2captcha로 reCaptcha 해결 자동화</h2>
<p><a href="https://2captcha.com/demo/recaptcha-v2?utm_medium=content&utm_source=outsider&utm_campaign=korea">2captcha에서 제공하는 데모 페이지</a>에서 테스트해보면서 사용 방법을 알 수 있다.</p>
<pre class="line-numbers"><code class="language-js">// retrive data from recaptcha
const recaptcha = await page.waitForSelector('.g-recaptcha');
const sitekey = await recaptcha.getAttribute('data-sitekey');
const currentUrl = await page.url();
</code></pre>
<p>reCaptcha v2의 정보가 필요하므로 <code>.g-recaptcha</code> 요소에 <code>data-sitekey</code> 속성으로 저장된 키가 필요하고 현재 URL이 필요하기 때문에 이 값을 가져왔다.</p>
<p>이제 이를 이용해서 2captcha에 요청을 보낼 차례이다.</p>
<pre class="line-numbers"><code class="language-js">// 2captcha
const APIKEY = process.env.APIKEY;
const twoCaptchaURL = `https://2captcha.com/in.php?key=${APIKEY}&method=userrecaptcha&googlekey=${sitekey}&pageurl=${currentUrl}`;
let requestId;
try {
const response = await got(twoCaptchaURL);
const result = response.body;
if (result.startsWith('OK|')) {
requestId = result.split('|')[1];
console.log('RequestId: ' + requestId);
} else {
throw new Error(`Wrong response: ${result}`);
}
} catch(e) {
console.log(e.response.body);
await browser.close();
}
</code></pre>
<p>2captcha의 API Key는 <a href="https://2captcha.com/enterpage?utm_medium=content&utm_source=outsider&utm_campaign=korea">대시보드</a>에서 볼 수 있다. 이를 이용해서 CAPTCHA 해결 요청 URL인 <code>https://2captcha.com/in.php</code>에 보내면서 앞에서 저장해 놓은 <code>sitekey</code>와 <code>currentUrl</code>을 파라미터로 보낸다.</p>
<p>응답이 제대로 처리된 경우 <code>OK|66648692707</code> 같은 형식의 응답을 받으므로 앞의 <code>OK|</code> 부분을 잘라내고 뒷부분인 요청 ID를 따로 저장했다. 해당 요청이 처리되었는지 확인하려면 이 요청 ID가 필요하다.</p>
<pre class="line-numbers"><code class="language-js">const resultUrl = `https://2captcha.com/res.php?key=${APIKEY}&action=get&id=${requestId}`;
const getCaptchaResult = () => {
return new Promise((resolve, reject) => {
setTimeout(async () => {
try {
const response = await got(resultUrl);
resolve(response.body);
} catch(e) {
reject(e.response.body);
}
}, 10000);
});
};
let answer;
let isNotSolved = true;
while(isNotSolved) {
try {
const result = await getCaptchaResult();
if (result.startsWith('OK|')) {
answer = result.split('|')[1];
isNotSolved = false;
} else {
throw new Error(`Wrong response: ${result}`);
}
console.log(result);
} catch(e) {
console.log('error')
console.log(e);
}
}
</code></pre>
<p>해당 CAPTCHA의 해결은 실제로 사람이 하므로 시간이 걸리고 언제 완료되는지 알 수가 없다. 그러므로 요청 ID를 가지고 주기적으로 확인해 보면서 해당 요청이 처리되었는지 확인해야 한다. 이를 확인하는 함수가 <code>getCaptchaResult()</code>이고 이를 <code>while(isNotSolved) {}</code>으로 10초마다 계속 호출하면서 요청이 처리되었는지를 확인한다. 요청이 아직 처리 안 되었을 때는 <code>CAPCHA_NOT_READY</code> 응답이 오고 요청이 완료되면 <code>OK|03AGdBq25rA1ntkj4Q67Zj...</code>처럼 <code>OK|</code> 뒤에 결과값이 오므로 이를 추출해서 가져온다.</p>
<pre class="line-numbers"><code class="language-js">// enter recaptcha answer
await page.$eval('#g-recaptcha-response', el => {
el.style.display = 'block';
});
await page.fill('#g-recaptcha-response', answer, {delay: 30});
await page.evaluate((answer) => {
handleCaptcha(answer);
}, answer);
setTimeout(async () => {
await page.screenshot({ path: 'img/complated.png' });
await browser.close();
}, 5000);
</code></pre>
<p>이제 해결된 reCaptcha의 결과값을 처리해줘야 한다 reCaptcha v2의 경우 <code>g-recaptcha-response</code>를 ID로 가진 <code><textarea></code>에 이 결과값(<code>answer</code>)를 입력해주어야 한다. 해당 <code><textarea></code>가 보이지 않으면 입력할 수 없으므로 <code>display: block;</code>으로 보이게 처리한 뒤에 값을 입력한다.</p>
<p>기본적으로는 해당 폼을 제출하면 처리되어야 하지만 월마트의 경우 자동화를 막기 위해서인지 그냥 <code><form></code> 제출로는 처리되지 않고 해당 <code><form></code>을 처리하는 별도의 함수 <code>handleCaptcha()</code>를 만들어 두었기 때문에 이 함수에 <code>answer</code>를 전달해서 호출한다. 호출할 때는 페이지에서 JavaScript가 실행되도록 <code>page.evaluate()</code>를 사용했다. 폼 제출만으로 처리되면 좋겠지만 월마트의 경우처럼 대부분의 사이트는 자동화를 막기 위한 다양한 추가 트릭을 제공할 것이므로 이 부분은 상황에 맞게 각자 사이트를 분석해서 처리해야 한다.</p>
<p>마지막은 로그인된 페이지의 스크린숏을 찍기 위해서 <code>setTimeout()</code>으로 대기한 후에 스크린숏을 찍고 <code>browser.close()</code>로 브라우저를 종료했다.(해당 URL을 대기하는 식으로 구현해도 되지만 CAPTCHA 해결에 집중했기 때문에 이 부분은 테스트용으로 작성했던 것을 그대로 두었다.)</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/5182446275.gif" width="749" height="441" alt="월마트 로그인을 자동화한 영상" title="" /></p>
<p>headless 옵션을 풀고 실행하면 실제로 크롬을 실행해서 동작하는 것을 볼 수 있는데 위처럼 동작한다. 로그가 찍히는 것을 볼 수 있도록 뒤에 터미널의 로그를 볼 수 있게 했고 응답을 기다리다가 완료되면 처리하는 것을 볼 수 있다. 마지막에 페이지가 뜨자마자 끝나기는 하는데 아래처럼 로그인된 화면이 뜨는 것을 확인할 수 있다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/7784079971.jpg" width="750" height="422" alt="로그인된 월마트 페이지" title="" /></p>
<p>CAPTCHA를 직접 해결하거나 우회하려면 비용이 많이 들고 창과 방패처럼 자동화를 피하려는 사이트의 작업이 계속되는 만큼 이를 해결하려는 노력도 계속해야 하는데 그 가운데 다루기 어려운 CAPTCHA는 2captcha를 이용하면 적은 비용으로 쉽게 해결할 수 있다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1547?commentInput=true#entry1547WriteComment">댓글 쓰기</a></strong></p>AngularJS-Atom v0.1.0 릴리즈Outsiderhttps://blog.outsider.ne.kr/11152015-01-02T04:27:12+09:002015-01-02T04:25:44+09:00<p><a href="https://angularjs.org/">AngularJS</a>와 관련된 프로젝트를 개발하는 <a href="https://github.com/angular-ui">AngularUI</a>팀에 합류하고 나서는 <a href="https://github.com/angular-ui/AngularJS-Atom">AngularJS-Atom</a>에 손을 거의 못 대고 있었다. 그동안 <a href="https://github.com/angular-ui">AngularUI</a>팀에서 잘릴까 봐 걱정도 꽤 했지만, 다행히 그러진 않았다. AngularUI 하위에 프로젝트가 많다 보니 각 프로젝트는 그냥 프로젝트 담당자에게 맡기고 거의 건드리지 않는다. 나도 처음 합류하고 커밋했을 때만 거의 라인단위로 리뷰를 하고 이슈에만 댓글을 달아주고는 내가 릴리즈는 어떻게 해야 하나 고민하자 알아서 하면 된다고 한 뒤로는 거의 건드리지 않는다.<br />
<br></p>
<h1>v0.1.0</h1>
<p>스니펫과 자동완성 등을 주로 지원하다가 9월에 리포팅 받은 버그를 좀 수정하고 <a href="https://github.com/angular-ui/AngularJS-Atom/releases/tag/0.0.5">v0.0.5</a>를 릴리즈 하고 나서는 거의 손을 못 대고 있었다. 그럼에도 그 사이에 15,000번이나 다운받는 패키지가 되어서 새 버전을 릴리즈 해야 한다는 압박감도 꽤 있었다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/1098697924.gif" width="750" height="213" alt="AngularJS-Atom의 다운로드 횟수" title="" /></p>
<p>그사이에 이슈로 올라온 내용 중에는 <code>ng-*</code>같은 디렉티브에 신텍스 하이라이트를 해달라는 <a href="https://github.com/angular-ui/AngularJS-Atom/issues/13">이슈</a>가 제일 큰 요구사항이었지만 이걸 어떻게 해야 하는지 알 수가 없었다. <a href="https://atom.io/">Atom</a>은 HTML을 사용하므로 에디터 내의 작성하는 코드도 HTML로 되어 있는데 특정 키워드에 신택스 하이라이트를 추가하려면 그 키워드에 새로운 class를 추가해야 하는데 이걸 어떻게 해야 하는지 몰랐다. 도움 요청도 해봤지만(forum에 물어볼 걸 그랬나..) <code><p></code>태그에 CSS를 주라는 엉성한 대답만 돌아왔고 여러 번 시도했지만, 방법을 찾지 못했다.</p>
<p>비슷한 류의 패키지의 소스를 참고하다가 <a href="https://atom.io/docs/v0.165.0/creating-a-package#language-grammars">grammars</a>를 사용해서 신택스 하이라이트를 추가할 수 있다는 것을 깨닫게 되었다. Atom은 <a href="http://macromates.com/">TextMate</a>의 규칙을 많이 따르고 있는데 그래머도 TextMate의 방식을 그대로 따르고 있었고 Atom 문서에도 <a href="http://manual.macromates.com/en/language_grammars.html">TextMate의 그래머 문서</a>를 보라고 안내가 되어 있어서 다 읽어봤다. Atom은 문서화가 아직 엄청나게 부실하다.</p>
<p>결국, 그래머로 어느 정도 해결은 했지만, 기존 HTML 문법을 오버라이딩하는 방법을 찾지 못해서 또 한참 고생하다가 TextMate쪽으로 뒤져서 겨우 방법을 찾아냈다. 아.. 문서화가 정말 너무 부족하다. 어쨌든 한참의 삽질을 거쳐서 원하는 형태로 신택스 하이라이트를 추가하는 방법을 찾아냈고 추가로 Angular Expressions에도 신택스 하이라이트를 추가했다.</p>
<p>그동안 Atom의 방식을 제대로 이해 못 해서 간단한 스니펫과 자동완성만 지원하고 있어서 테스트도 작성하지 않았는데 이번 업데이트로 구조도 꽤 많이 파악하고 테스트도 추가했다. 조금씩 모양이 갖춰져 가고 있다. 새로운 기능도 추가하고 했기에 이번에 마이너 버전을 올려서 <a href="https://github.com/angular-ui/AngularJS-Atom/releases/tag/0.1.0">v0.1.0</a>으로 릴리즈를 했다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1115?commentInput=true#entry1115WriteComment">댓글 쓰기</a></strong></p>angular-summernote v0.2.4 릴리즈Outsiderhttps://blog.outsider.ne.kr/10862014-10-04T23:32:25+09:002014-10-04T23:32:25+09:00<p><a href="http://blog.outsider.ne.kr/1075">지난번 릴리즈</a> 후 한 달 정도 만에 <a href="https://github.com/outsideris/angular-summernote/releases/tag/0.2.4">새버전을 릴리즈</a>했다. 여름 이후부터는 사용자들이 좀 있는지 저번에 이슈를 다 털었는데도 그 사이에 이슈가 몇 개 올라왔다. 그래서 이번 릴리즈는 마이너 버그 수정 버전이다. 버그 하나 수정하고 릴리즈하는 게 좀 애매하기는 했지만, 오류가 발생하는 거라서 수정을 했다.</p>
<p><a href="https://github.com/outsideris/angular-summernote/issues/19">이슈</a>는 <code>ng-repeat</code>사이에서 동적으로 summernote를 생성하고 제거하는데 제거할 때 오류가 발생한다는 내용이었다. 이슈가 잘 이해 안 돼서 코드를 받아서 테스트해보았더니 실제로 발생했다. 추적을 해보니 Angular.js의 스코프가 제거될 때 summernote를 제거하기 위해서 <code>destroy()</code>함수를 실행하는데 여기서 summernote DOM에 바인딩해 놓은 옵션객체를 <code>data()</code>로 받아와서 확인하는데 <code>data()</code>가 빈 객체로 나왔다.</p>
<p><code>$scope.$on('$destroy')</code>로 이벤트를 받아서 summernote를 제거하는데 테스트해보니 <code>data()</code> 객체가 계속 정상적으로 있다가 이 이벤트로 받아서 넘어오면 빈 객체로 바뀌어 버린다. angular.js 소스를 추적해 봐도 정확한 원인까지는 찾지 못했는데 삭제 전에 DOM 객체를 제거하고(혹은 어떤 정리작업) 넘겨주는데 이때 <code>data()</code>로 바인딩한 값이 없어지는 것 같다.(이게 버그인지 의도인지는 잘 모르겠다.)</p>
<p><a href="https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$destroy">$destroy 이벤트의 문서</a>를 보면 다음과 같이 나와 있다.</p>
<blockquote>
<p>Note that, in AngularJS, there is also a $destroy jQuery event, which can be used to clean up DOM bindings before an element is removed from the DOM.</p>
</blockquote>
<p><code>$destroy</code>는 스코프 내에서 이벤트를 브로드캐스팅하지만 동시에 <code>$destroy</code> jQuery 이벤트도 발생시키므로 둘 다 사용할 수 있다. 정확히는 <code>$destroy</code> 이벤트가 먼저 발생하고 이후에 Angular.js의 <code>$destroy</code> 이벤트가 브로드캐스팅된다. 그래서 <code>$(element).on('$destroy')</code>로 이벤트를 받으니까 <code>data()</code>에 옵션객체가 제대로 존재에서 오류 없이 <code>ng-repeat</code>내에서 오류 없이 제거할 수 있었다.</p>
<p>다만 기존에 summernote 삭제 테스트를 작성해 놓은 부분이 있어서 <code>$scope.destroy();</code>로 직접 제거하는 테스트코드가 있었는데 이때는 jQuery 이벤트가 발생하지 않아서 summernote가 제거되지 않았다. 결국은 별수 없이 jQuery <code>$destroy</code>이벤트 내에서 삭제를 나타내는 플래그를 하나 추가하고 Angular.js의 <code>$destroy</code>이벤트에서 이 플래그가 없을 때만 제거하도록 했다.(꼼수 같은 기분이 들어서 angular.js를 더 공부해야겠다. ㅠㅠ)</p>
<p>Angular.js가 1.3.0이 RC에 진입해서 다음 수정은 1.3.0 호환성 작업이 될 것 같다. Angular 버전도 이제 여러 버전에서 테스트해 돌려야 하는데 어떻게 환경 설정하는지도 찾아봐야 할 듯...(Travis ci에서는 건드리지도 않은 테스트가 왜 깨져 ㅠㅠ )</p>
<p><strong><a href="https://blog.outsider.ne.kr/1086?commentInput=true#entry1086WriteComment">댓글 쓰기</a></strong></p>angular-summernote v0.2.3 릴리즈Outsiderhttps://blog.outsider.ne.kr/10752014-09-06T16:53:26+09:002014-09-06T16:53:26+09:00<p>전에는 개인 프로젝트를 해도 어떤 릴리즈 시점이 없이 하고 있었는데 최근에는 메인테이너로 있는 소소한 프로젝트가 있어서 릴리즈할 때 블로그에도 공유하기로 했다. 릴리즈 공지용 페이지를 따로 운영하기에는 너무 소소하고 그렇게 잦은 릴리즈를 할 것도 아니라서...</p>
<p><a href="https://github.com/outsideris/angular-summernote">angular-summernote</a>는 올 초에 <a href="http://hackerwins.github.io/summernote/">summernote</a>와 <a href="https://angularjs.org/">angular.js</a>의 인기에 업혀가려고 <a href="http://blog.outsider.ne.kr/1019">후딱 만들었던 프로젝트</a>다. 이번에 며칠 간 작업을 해서 <a href="https://github.com/outsideris/angular-summernote/releases/tag/0.2.3">v0.2.3</a>을 릴리즈 했다.</p>
<p>초기에 큰 이슈들만 처리하고 어느 정도 안정화가 된 상태에서 놔두고 있었는데 지난 5월부터 이슈가 가끔 올라왔는데 회사 일이 바빠서 처리를 못 하고 있었다. 최근에 사용하는 사람들이 약간 늘었는지 아니면 전에는 이슈를 좀 바로바로 처리하다가 몇 달씩 놔둬서 그런지 이슈가 쌓여가기 시작했고 언제 수정해 주느냐는 불평의 댓글도 올라왔다.(왠지 이런 댓글을 보니 더 처리하기 싫어지기도 했다.) 계속 처리 못 하고 있으니 <a href="https://github.com/outsideris/angular-summernote/issues/15">몇몇은 자신이 어떻게 패치해서 수정하고 있는지 공유하는 훈훈한 광경</a>도 나왔다.(그래도 풀 리퀘스트를 만들어서 보내주는 사람은 없더군.)</p>
<p>이번 수정에서는 버그픽스가 주 작업이었다.(그래서 버전은 패치버전만 하나 올렸다.) 가장 큰 버그가 <code>onImageUpload</code>시에 <code>ngModel</code>이 동기화되지 않는 현상이었는데 업로드는 테스트하기 어려워서 제대로 테스트 안했더니 모달팝업때문에 <code>ngModel</code>에 변경사항이 제대로 반영되지 않았다. 처음 만들 때는 summernote에 <code>onChange</code> 이벤트가 없어서 변경사항을 추적하기 위해서 <code>keyup</code>등의 이벤트를 추적해서 변경사항을 반영하고 있었는데 그 사이에 summenote가 v0.5.8로 올라오고 <code>onChange</code> 이벤트도 생겨서 일단 최신 버전에 맞게 코드를 고쳤다. 너무 오랜만에 만지는 거라 소스를 다시 파악할 필요가 있기도 했고...</p>
<p><code>onChange</code>로 코드를 변경하고 나니(여기서도 고생을 꽤 했지만) <code>onImageUpload</code>를 적용해야 했는데 사실 이 부분은 로직은 금세 짰지만, 테스트를 짜는데 엄청나게 오래 걸렸다. summernote 자체의 코드는 모르기도 했고 <a href="http://karma-runner.github.io/0.12/index.html">karma</a>에서 업로드동작을 구현하려니 이벤트가 발생하지 않아서 꽤 고생했다. 이것만 며칠 붙잡고 있었던 듯.. ㅠㅠ (사실 <code>onImageUpload</code> 리스너 테스트는 아직 작성하지 못했다. 아~ 파일 업로드 ㅠ)</p>
<p>버그를 다 수정하고 최신 기능 중 하나인 airmode 기능도 추가하고 새로운 버전을 릴리즈햇다. <a href="https://github.com/outsideris/angular-summernote/blob/master/CHANGELOG.md">change log</a>를 따로 관리하고 있기는 하지만 이젠 Github의 <a href="https://github.com/outsideris/angular-summernote/releases">릴리즈 기능</a>에서 change log를 관리해 주기 때문에 파일로 관리할 필요가 있나 생각 중이다.</p>
<p>이번 수정을 하면서 이슈도 완전히 털어버렸더니 기분이 상쾌하다. 작업을 시작한 김에 커버리지도 봐야겠다고 생각해서 <a href="https://coveralls.io/">Coveralls</a>도 붙였다.(<a href="http://blog.outsider.ne.kr/954">Github에서 코드 커버리지를 보여주는 Coveralls</a> 참고) <a href="https://coveralls.io/docs/javascript">Coveralls 문서</a>에서 몇 가지 플러그인을 안내하고 있는데 마친 Karma로 테스트를 작성하고 있어서 <a href="https://github.com/mattjmorrison/grunt-karma-coveralls">grunt-karma-coveralls</a>를 사용했다. grunt-karma-coveralls는 <a href="https://github.com/karma-runner/karma-coverage">karma-coverage</a>에 기반을 두고 있는데(커버리지는 <a href="https://github.com/yahoo/istanbul">istanbul</a>로 측정한다) 처음 써봐서 약간 시간이 걸렸지만 몇 가지 설정을 적용하니 커버리지를 측정을 연동할 수 있었다. 몇 줄 안 되는 코드이지만 충실히 테스트를 작성한 결과 <a href="https://coveralls.io/r/outsideris/angular-summernote">90.57%</a>의 커버리지가 나왔다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/3241041979.gif" width="750" height="56" alt="Github README의 프로젝트 빌드 상태 배지" title="" /></p>
<p>전에 오류 나던 Travis CI의 테스트도 해결돼서 배지가 모두 녹색으로 나와서 기분이 좋다.</p>
<p>덧) 릴리즈 공지를 쓰려고 한 거였는데 쓰고 보니 후기처럼 써버렸;;; ㄷㄷㄷ</p>
<p><strong><a href="https://blog.outsider.ne.kr/1075?commentInput=true#entry1075WriteComment">댓글 쓰기</a></strong></p>d3.bayarea() 유저그룹 Meetup 후기Outsiderhttps://blog.outsider.ne.kr/9922013-10-27T23:57:34+09:002013-10-27T23:57:34+09:00<p>컨퍼런스가 있어서 샌프란시스코에 출장을 갔다왔는데 주말을 이용해서 출장일정보다 조금 더 일찍 갔다왔다. 여유시간동안 쇼핑도 하고 여기 저기 돌아다녔지만 관광이 주 목적이 아니었기에 <a href="http://www.meetup.com/">meetup</a>사이트에 가서 갈만한게 있는지 계속 검색해 봤다.(나중에 보니 <a href="http://www.eventbrite.co.uk/">Eventbrite</a>도 같이 검색해 보는게 좋을듯...) 사실 혼자가서는 외국인과 대화할 일이 별로 없기 때문에 꼭 기술관련 밋업이 아니더라도 갈만한게 있으면 갈 생각이었지만 딱히 갈만한 건 눈에 띄지 않았는데 <a href="http://www.meetup.com/Bay-Area-d3-User-Group/">베이지역의 d3 유저그룹</a>에서 <a href="http://www.meetup.com/Bay-Area-d3-User-Group/events/137545992/">d3 dat app</a>이라는 밋업을 한다는 것을 알게 되어서 갔다가 왔다.</p>
<p><a href="http://d3js.org/">d3.js</a>는 관심기술이기도 했으니 참가하기 딱 좋은 밋업이었다. 사실 신청할 때 대기자가 몇십명정도 있어서 기대안하고 신청버튼을 눌렀는데 내가 잘못봤는지 어땠는지 참가승인이 나서 낮에 관광을 갖다가 와서 저녁에 냉큼 다녀왔다. 뭐 관광지라서 저녁에 걸어다녀도 위험하진 않았다. 뭐 아무래도 영어로 발표하다 보니 다 이해하진 못했고 대충이나마 적고 내가 잘못 이해하거나 중요한 부분을 빠뜨렸을 가능성도 높다.<br />
<br></p>
<h1>Application building with D3 - <a href="https://twitter.com/jfire">John Firebaugh</a></h1>
<ul>
<li><a href="http://jfire.io/presentations/graphical-web-2013/#/1">발표자료</a></li>
</ul>
<p>이 발표는 밋업에서 봤지만 아마도 <a href="http://html5devconf.com/">HTML5 Developer Conference</a>에서 발표한 내용과 동일한 듯 한다. 이 발표는 <a href="http://www.openstreetmap.org/">오픈스트리트맵</a>의 맵 에디터로 d3기반으로 만든 <a href="http://ideditor.com/">iD</a>라는 맵 에디터에 대한 얘기였다. 일반적으로는 jQuery나 Backbone.js등으로 웹 어플리케이션을 만들고 좀 더 최신기술을 사용하는 경우에는 ember나 angular.js를 사용하지만 iD는 이런 것들이 아니라 d3.js를 기반으로 만들었다. jQuery와 d3를 비교했을 때 Dimension관련 부분은 jQuery에만 있지만 데이터 바인딩은 D3에만 있고 그 외 Ajax, CSS, 이벤트등 대부분의 기능은 양쪽 모두에 존재한다.</p>
<p>d3.js를 선택한 이유는 선언적인 방법이 명령형(imperative) 방법보다 좋기 때문이고 jQuery가 명령형이라면 d3.js는 선언적이다. 프레임워크는 비즈니스 로직은 프리젠테이션 로직에서 분리하기 쉽게 해주는데 backbone.js는 이부분이 개발자의 몫이고 ember나 angular.js는 스마트 템플릿을 사용하고 있다. d3에서는 3단계로 이뤄지는데 <strong>"진입(enter) -> 갱신(update) -> 종료(exit)"</strong>로 이뤄진다. 여기서 작업을 최소화 하기 위해서는 다음 규칙을 따르면 된다.<br />
<br></p>
<ul>
<li>정적 프로퍼티는 진입 단계에서 선언한다.</li>
<li>동적 프로퍼티는 갱신 단계에서 선언한다.</li>
<li>대량 갱신은 개별적인 "instance changed" 이벤트를 사용하지 않는다.</li>
<li>실제로 변경되는 요소의 선택을 필터링한다.</li>
</ul>
<p>iD에서 모델은 불변객체이므로 변경하지 않고 변경해야 할 경우에는 새로운 객체로 만들고 히스토리를 관리하고 있다.</p>
<p>그리고 d3.js의 단점 중 하나는 너무 크다는 것인데 이는 <a href="https://github.com/mbostock/smash">smash</a>로 필요한 파일들만 가져와서 빌드할 수 있다.(써본적은 없지만 smash도 d3.js를 만든 Mike Bostock이 만든 도구로 이미 d3.js도 이를 사용해서 빌드하고 있다.) 앞에서도 말했듯이 dimension관련 부분이 d3.js에 없는 것도 하나의 단점인데 이는 직접 플러그인을 작성할 수 있고 d3 플러그인은 <a href="https://github.com/d3/d3-plugins">d3-plugins</a>나 <a href="https://github.com/shawnbot/d3-bootstrap">d3-bootstrap</a>에서 찾아볼 수 있다.</p>
<p><strong>Pick technology that fits your problem domain.</strong><br />
<br></p>
<h1>.append() IT TO THE LIMIT - HIGH performance d3 - <a href="https://twitter.com/vicapow">Victor Powell</a></h1>
<ul>
<li><a href="https://github.com/vicapow/append-it-to-the-limit">발표자료 & 데모</a></li>
</ul>
<p>d3 사용할 때 퍼포펀스 향상에 대한 얘기였는데 d3를 자세히 몰라서 그런지 제대로 잘 이해해지는 못했다. 최적화를 할때는 언제 최적화를 하고 언제 최적화를 하지 않을 것인지가 중요한데 이는 대부분 개발자 시간이 CPU 시간보다 크기 때문이다.</p>
<blockquote>
<p>"Premature optimization is the root of all evil" - 도널드 크누스</p>
</blockquote>
<p><code>setInterval()</code>보다는 <code>requestAnimationFrame()</code>를 이용한 <code>transition()</code>이 낫고 크기를 변경할 때는 redraw하는 것보다 <code>scala(X,Y)</code>이 낫고 <code>transform</code>보다는 <code>cx</code>, <code>cy</code>를 이용하는게 빠르다. 그리고 왠만하면 DOM은 건드리지 않는게 좋은데 특히 계산된 프로퍼티는 조작하지 않는 것이 좋다. 그리고 값을 변경할 때 변경되지 않은 요소들은 미리 필터링하는 것이 성능에 좋다.<br />
<br></p>
<h1>기타...</h1>
<p>메인 발표는 위의 두 가지였고 유저그룹이다 보니 이후부터는 공유할 거리가 있는 사람들이 5분 10분씩 나와서 자신들이 한 작업들을 공유했는데 유저그룹에서 이런 진행은 자유스럽고 좋아보였다.</p>
<p><a href="http://twitter.com/saraquigley">Sara Quigley</a>가 <a href="http://saraquigley.github.io/uc-trends/">University of California System Campuses</a>라는 캘리포니아 대학의 예산관련 비쥬얼라이제이션을 보여주었는데 그래프와 인터렉션을 깔끔하게 잘 만들었다. 이 인터렉션을 위해서 사용자가 어떤 값을 선택할 때 인터렉션을 보여주는 전략에 대해서 <a href="http://bl.ocks.org/saraquigley/6628958">Strategic Dilemmas</a>와 <a href="http://bl.ocks.org/saraquigley/6631328">Strategy Spaces</a>를 소개해 줬는데 잘 모르는 분야라 흥미로웠다.</p>
<p><a href="http://www.crowdsound.io/beta">CrowdSound</a>라는 사이트를 준비하고 있는걸 보여주었는데 컨퍼런스 같은데서 청중들이 질문들을 올리고 발표자가 여기에 피드백할 수 있는(백채널이라고 하나?) 시스템에 대해서 보여주었는데 딱히 큰 건 없었다.</p>
<p>이 유저그룹에서 샌프란시스코의 교통수단인 Bart에 대한 비쥬얼라이제이션 사이트 <a href="http://enjalot.github.io/bart/">Visualizing the BART Labor Dispute</a>를 보여주었는데 실질적인 데이터로 유저그룹에서 저런 사이트를 만든게 재미있었다.(서울데이터로도 저런걸 만들 수 있을라나?) <a href="https://github.com/Caged/d3-tip">d3-tip</a>을 쓰냐면서 좋으니까 꼭 쓰라고 했는데 이건 차트에서 툴팁 띄워줄대 유용할듯하다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/992?commentInput=true#entry992WriteComment">댓글 쓰기</a></strong></p>