nacyot profile image

주피터(Jupyter) 노트북과 자바스크립트 환경 이해하기 - 주피터 위에서 d3.js를 활용한 시각화

기존의 아이파이썬(IPython)에서 이제 본격적으로 주피터 프로젝트로 옮겨가는 과정이 한창 진행중이다. 주피터의 핵심에 대해서는 주피터 다중 커널 개념 이해하기에서 이미 다루었듯이, 파이썬을 비롯한 다양한 언어의 커널을 지원한다는 데 있다. 맥락은 조금 다르지만, 아이파이썬은 이미 훌륭한 자바스크립트 실행환경에서 작동한다는 점에서 주피터 이전에도 이미 멀티 커널을 기본적으로 지원하고 있었다고 할 수 있다. 이 글에서는 이러한 기능을 활용해 주피터 위에서 자바스크립트 코드를 작성 및 실행하고, d3.js 라이브러리를 통해 시각화를 하는 방법에 대해 간단히 소개한다.

노트북 만들기

먼저 주피터를 실행하고 Python3 커널로 노트북을 만든다.

Python3 커널 노트북 만들기

자바스크립트 매직

주피터 노트북에서는 매직 커맨드를 통해 단순히 파이썬 코드를 실행시키는 것 이외에도 다양한 작업을 수행할 수 있도록 도와준다. 이러한 매직 커맨드들은 언어 커널에 정의되어 있으면 파이썬 커널에서는 이를 위해 %%를 앞에 붙여서 실행한다. 예를 들어 %%html 매직을 사용하면 html 코드를 직접 결과 블록에 삽입할 수 있다. %%html을 첫 줄에 작성하고, 아래부터는 html코드를 작성한다.

68번째 입력:
%%html

<style>
.hello-world{
    border: 1px solid black; 
    width: 300px; 
    height: 50px; 
    font-size: 2em; 
    padding: 0.3em;
}
</style>

<div class='hello-world'>
Hello ,html magic!
</div>
Hello ,html magic!

위와 같이 html 코드를 바로 사용할 수 있음을 알 수 있다. 더욱 편리한 점은 주피터의 기본 에디터 CodeMirror는 다양한 언어를 지원하기 있어서, 주피터는 이를 통해 자동적으로 HTML 문법을 지원해준다.

이와 마찬가지로 자바스크립트 코드를 실행할 수 있도록 %%javascript 매직도 지원하고 있다. 여기서는 먼저 현재 주피터 셀의 결과 요소를 얻어올 수 있도록 헬퍼를 작성해서 사용한다(이 헬퍼가 필요한 이유에 대해서는 뒤에서 자세히 설명한다).

69번째 입력:
%%javascript

window.get_element = function(el){
    if(el){ $(el).html('') }
    return (el !== undefined) ? el[0] : $('script').last().parent()[0];
};

element = undefined;
70번째 입력:
%%javascript

var target = get_element(element)
$(target).append('<div class="hello-world">Hello, js magic!</div>')

자바스크립트 블록도 정상적으로 출력이 된다. 주피터 노트북은 웹브라우저에서 작동하기 때문에 커널을 통해서 파이썬 코드를 평가하는 것은 물론, 이제 %%html%%javascript 매직을 통해서 자유롭게 웹 페이지의 요소들을 다룰 수 있다.

헬퍼 함수 이해하기

앞선 예제에서는 get_element라는 헬퍼 함수를 작성했다. 이 함수를 이해하기에 앞서 한가지 짚고 넘어갈 부분이 있다. 주피터 노트북의 %%javascript 매직 안에서는 element라는 특별한 요소가 정의되어있다. 이 element에는 현재 주피터 노트북 셀의 결과 요소가 지정되어 있고, 이를 조작해서 현재 셀의 출력 결과에 대한 조작을 할 수 있다.

예를 들어서 jupyter notebook 위에서는 헬퍼 함수 없이도 다음과 같이 코드가 정상적으로 작동한다.

$(element).append('<div class="hello-world">I don't need helper!</div>')

element를 활용한 자바스크립트 평가

일반적이지 않은 매직 셀을 평범한 셀처럼 사용할 수 있게 해주는 흥미로운 부분이다. 하지만 주피터 노트북의 저장 포맷인 ipynb 파일에는 자바스크립트 매직 셀의 실행 결과가 저장되지는 않는다. 이는 단지 브라우저 위에서 실행될 뿐이다. ipynb에는 실행 결과가 저장되는 대신에 output 속성 아래에 다음과 같이 application/javascript 형식으로 소스코드 자체가 저장된다.

  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "application/javascript": [
       "$(element).append('<div class="hello-world">I don't need helper!</div>')",

(이하 생략)

이렇게 저장된 ipynb 파일은 jupyter notebook이나 nbviewer에서 파일을 읽었을 때 이 스크립트를 그대로 실행하는 방식으로 결과를 복원해준다.

jupyter notebook과 nbviewer 모두 지원하기

그런데 문제는 element에 있다. jupyter notebook에서는 코드를 직접 실행하거나, 저장된 파일을 읽어들여도 element 요소가 적절하게 해석된다. 하지만 nbviewer에서는 element 요소를 적절하게 해석하지 못 한다. 실제로 nbviewer에서 실행되는 코드를 살펴보자.

<div class="output_subarea output_javascript ">
<script type="text/javascript">
$(element).append('<div class="hello-world">I don't need helper!</div>')
</script>
</div>

nbviewer에서 ipynb 파일을 로드해서 코드를 열어보면 해당하는 output 위치에 이런 스크립트 태그가 들어가게 된다. 여기서는 element 요소가 없을 뿐더러, 더더욱 심각한 것은 이 스크립트 실행되고 있는 위치를 특정할 수 있는 어떠한 방법도 제공하지 않는다. 당연하지만 웹 브라우저는 element 요소를 찾을 수 없다는 에러를 내며 아무것도 보여주지 않는다. 주피터 노트북을 직접 사용할 수 있는 환경이라면 무방하지만, 이렇게 되면 nbviewer를 통해서 정적으로 노트북을 공유하는 것이 불가능해진다. 위에서 정의한 헬퍼함수는 바로 이러한 문제를 해결하기 위해서 만든 임시방편이다.

window.get_element = function(el){
    if(el){ $(el).html('') }
    return (el !== undefined) ? el[0] : $('script').last().parent()[0];
};

element = undefined;

이 함수는 element가 있는 환경과 없는 환경을 나눠서 출력 결과를 반환한다. element가 있으면 이를 그대로 사용하고, 없으면 현재 실행되고 있는 스크립트 태그를 찾아 그 부모를 반환하다. 이는 이 코드가 실행될 때 정의되어있는 마지막 스크립트 요소가 해당 코드를 포함한 script 요소라는 점을 활용한 약간은 편법적인 방법이다(이 방법 외에 코드가 실행되는 위치를 특정하는 방법을 찾기 쉽지 않았다).

이 헬퍼는 get_element(element)와 같이 사용하며, 이를 통해서 노트북과 뷰어 양 쪽 모두를 지원할 수 있다. 마지막 줄에서는 nbviewer에서 element가 정의되어 있지 않다는 에러를 방지하기 위해서 전역 환경 element 변수에 undefined를 대입한다. 이제 nbviewer에서도 정상적으로 작동하는 것을 볼 수 있다.

nbviewer에서도 실행 결과가 정상적으로 출력된다!

이제 자바스크립트를 사용해 노트북을 작성할 수 있음은 물론, 이를 정적으로 공유도 할 수 있게 되었다.

require.js 사용해 동적으로 외부 스크립트 사용하기

조금 더 나아가보자. 주피터 노트북에는 기본적으로 몇 가지 외부 스크립트들이 포함되어 있다. jquery, moment, require.js가 기본 외부 스크립트이다. 이러한 스크립트는 프로필 설정을 통해서 미리 추가할 수도 있지만, require.js가 눈에 띈다. 이를 사용하면 AMD 방식으로 동적으로 외부 스크립트를 읽어오는 것이 가능해진다. 이를 통해 미리 외부 스크립트들을 준비해야하는 번잡함과 전역 환경에 스크립트들을 로드해서 생길 수 있는 문제들을 피해갈 수 있다.

이를 사용하려면 먼저 외부 스크립트 주소를 require.config에 정의한다.

71번째 입력:
%%javascript

require.config({
    paths: {
        d3: "http://d3js.org/d3.v3.min"
    }
});

config에 정의한 d3 스크립트를 사용하려면 다음과 같이 require 함수에 사용하고자 하는 외부스크립트와 실행하려는 함수를 넘겨주면 된다.

require(['d3'], function(){
  // 이제 이 안에서는,
  // d3.js 라이브러리를 사용해 코드를 작성할 수 있다
});

스크립트가 제대로 로드되었는지 확인해보자.

클로저와 즉시실행함수 패턴을 활용해 출력 위치 보정하기

안타깝게도, 여기서도 위에서 이야기했던 것과 비슷한 문제가 하나 있다. require는 비동기적으로 실행되며 d3가 로드된 다음에서야 넘겨진 함수를 호출하도록 되어있다. 즉, d3가 로드되는 것을 기다리지 않고 다음 스크립트들을 실행해버린다. 주피터 노트북 위에서는 자바스크립트 실행에 대해서 독립된 영역이 사용되기 때문에 무방하지만, nbviewer에서는 위에서 보았듯이 모든 코드가 script 태그로 그냥 삽입된다. 따라서 그냥 실행하면 모든 변수가 전역에 노출되어 버린다. 따라서 target 변수는 실행하자마자 갱신되고, 이미 모든 코드가 실행되어버렸기 때문에 nbviewer에서 마지막에 호출된 get_element(element)가 모든 셀의 출력 위치가 된다. 아래는 여러 셀에서 출력한 모든 그래프가 마지막 셀에 그려져 버린 경우이다.

클로저를 사용하지 않은 경우

이 문제를 해결하려면 즉시실행함수 패턴과 클로저를 사용해서 미리 결과를 출력한 대상을 정의해두어야 한다. 즉 위에서 정의했던 코드는 다음과 같이 작성되어야 한다. 이를 통해서 함수로 각 셀의 실행 환경을 분리하고, targetElement를 정확히 지정할 수 있다.

(function(){
    var targetElement = get_element(element);

    require(['d3'], function(){
        // 이 안에서 d3.js 라이브러리를 사용하고,
        // targetElement를 통해서 결과를 출력한다
    });
})()

이제 모든 결과가 의도한 위치에서 출력될 것이다.

클로저를 사용한 경우

d3.js를 활용한 시각화

자잘한 문제들로 인해 조금 돌아서 왔다. 이제 d3.js를 실제로 사용해서 정말 간단하게 동그라미 몇 개를 실제로 그려보자.

먼저 require를 통해서 정말로 d3 객체를 읽어오는 지 확인해보자.

72번째 입력:
%%javascript

(function(){
    var targetElement = get_element(element);
    require(['d3'], function(){
        $(targetElement).append($('<p>' + d3 + '</p>'))
    });
})();

[object Object]가 나오는 것을 봐서는 무언가 읽어온 것을 알 수 있다. 이번엔 d3.js의 max 함수를 사용해보자.

73번째 입력:
%%javascript

(function(){
    var targetElement = get_element(element);
    require(['d3'], function(){
        $(targetElement).append($('<p>' + d3.max([3,91,82,34,19]) + '</p>'))
    });
})();

결과값이 정상적으로 출력된다. 이를 통해 d3.js가 정상적으로 로드되었다는 것을 알 수 있다.

이제 동그라미를 그려보자.

74번째 입력:
%%javascript

(function(){
    var targetElement = get_element(element);
    require(['d3'], function(){
        var data = [1, 2, 3, 4, 5, 6, 10]
        var svg = d3.select(targetElement).append('svg')
            .attr('width', '350px')
            .attr('height', '100px')
            .style('border', '1px solid lightgray');
        
        svg.selectAll('circle')
            .data(data)
            .enter()
            .append('circle')
            .style('fill', 'skyblue')
            .attr('cx', function(d, i){ return i * (350/data.length) + 15})
            .attr('cy', '50px')
            .attr('r', function(d){ return d * 3})
    });
})();

정말 그냥 데이터 배열을 통해서 생성한 동그라미다.

결론

이를 통해 자바스크립트와 d3.js를 활용해(다른 라이브러리 어떤 것이라도 가능하다) 노트북을 작성할 수 있다. 아직 nbviewer에서는 이런 부분에 대한 지원이 약한 편인데(앞으로 지원 여부도 명확하지 않다), 약간의 편법을 통해서 피해갈 수 있다. 아래 링크들은 실제로 이러한 방식을 사용해서 만들어본 몇 가지 예제들이다. 모두 nbviewer를 통해서 웹에서 직접 확인해볼 수 있다. 심지어(?) 지금 읽고 있는 글도 nbviewer에서 직접 확인할 수 있다.


comments powered by Disqus