이직한지 2주째… 얌전히 눈치를 보며 팀의 안살림을 하다가, 뜬금없이 D3.js를 리서치하게 되었다. 그 동안 D3.js는 막연하게 차트를 예쁘게 만들어주는 라이브러리라고만 알고있었다(더불어 입문자의 두개골을 많이 쪼개왔다는 것도). 개발자의 본업삽질에 충실하기 위해 아무생각없이 접한 D3.js는 역시나 만만한 라이브러리가 아니었다. 그래서 처음 접했을 때 어려웠던 부분, 간단한 부분인데 어이없이 삽질했던 부분등을 공유하여 다른분들의 삽질을 줄여주고 싶다.
1. D3.js
D3.js 문서의 첫 번째 장을 보면 D3.js is a JavaScript library for manipulating documents based on data.
라고 쓰여있다. 이 줄을 읽자마자 그동안의 오해가 모두 사라졌는데, D3.js는 차트를 만들어 주는 라이브러리가 아니라는 것이다. D3.js는 data를 dom에 binding해주고 그것을 기반으로 HTML, svg, css를 조작할 수 있게 도와준다(차트와는 다르다 차트와는).
그렇기 때문에, 단순히 D3.js에 데이터를 바인딩해주는 정도로는 아무 일도 일어나지 않는다. DOM object들을 이용해서 데이터에 맞는 그림
을 직접 그려주어야 한다.
2. D3 걸음마
먼저 svg를 만드는 간단한 예제를 보면 몇 가지 원리를 이해할 수 있다.
<!-- before -->
<body></body>
<script>
d3.select('body').append('svg').attr('width', 800).attr('height', 300)
</script>
<!-- after -->
<body>
<svg width="800" height="300"></svg>
</body>
- selector
body를 선택 - append
body 밑에 svg를 추가 - attr
svg에 각각의 attribute를 추가
간단하다!
3. selection
D3의 기본은 selection이라는 개념에서 시작한다. 말 그대로 DOM의 요소를 선택하는 것으로, 우리가 익히 아는 방법으로 수월하게 사용할 수 있다. CSS 선택자를 사용하는 방법이 그것이다.
d3.select('div')
d3.select('div.test')
d3.select('div.test:first-child')
위의 코드 모두 잘 동작하는 코드이다. 다만 select
함수는 선택하려는 대상이 여러개라도 단 하나의 element를 return한다. 만약 test라는 class가 선언되어있는 div 태그를 모두 선택하려면 selectAll
을 사용하면 된다. selectAll
을 사용하게되면, 선택된 모든 요소를 배열로 반환한다.
d3.selectAll('div.test')
selector를 이용해 DOM을 선택하고 나면, 보통은 여러가지 modifier들을 이용한다. 보통의 경우 modifier들은 한 개 혹은 두 개의 parameter를 받으며, 하나의 parameter만 받는 경우 getter, 두 개 다 받게되면 setter 처럼 동작하게 된다(반드시 그런 것은 아니지만 그런 느낌으로 받아들이면 된다).
d3.select('svg').selectAll('rect').attr('width', 3).classed('test', true).style('font-size', '10px')
이 경우, 모든 선택된 rect
에 대해 width, class, style이 적용된다.
두 번째 parameter에 함수를 넣어 이용하는 방법도 있다. 예를 들면, 특정 rect
에 attribute를 추가하고 싶을때 사용하는 것이다.
d3.select('svg')
.selectAll('rect')
.attr('width', 3)
.classed('test', true)
.style('font-size', '10px')
.attr('fill', function (d) {
return d3.select(this).classed('test') ? 'blue' : null
})
4. Data binding
DOM을 selection했다면 적당한 데이터를 binding해주어야 한다. 어찌보면 D3.js의 핵심 컨셉이며, 어떻게 데이터를 binding하느냐에 따라 그릴 수 있는 DOM의 모습이 달라지기때문에 중요한 부분이라고 할 수 있다. 기본적으로 data를 binding하는 방법은 예상한 대로 간단한다.
<body>
<svg>
<rect></rect>
<rect></rect>
<rect></rect>
</svg>
</body>
<script>
const data = [1, 2, 3]
const rects = d3.selectAll('rect').data(data)
</script>
이렇게 하면, 각 rect
element에 data
배열에 들어있는 요소들이 차례대로 binding된다. Binding된 데이터를 확인하려면, 간단히 rects
를 출력해 보면 된다.
console.log(rects)
/** [Array(5)]
0: Array(5)
0: rect
...
__data__: 1 // binded data
__proto__: SVGRectElement
*/
이렇게 DOM에 binding된 데이터들은 나중에 svg attribute를 통한 시각화를 하는데에 중요하게 쓰이게 된다.
그런데 여기서 두가지 의문이 생긴다. Binding하는 데이터의 수만큼, 그에 해당하는 rect
를 미리 html에 선언해 두어야 하는가? 라는 물음이 첫번째 이다. 만약 그렇다면 600개의 데이터를 binding해야한다고 가정할 때, 우리는 html 파일에 600개의 rect
를 미리 배치해 두어야 한다.
두 번째는 binding되는 데이터가 동적으로 변할 때 이다. 600개의 rect
가 미리 배치되어 있고, 590개의 데이터가 binding될 때는 조금 상황이 나을수 도 있지만, 만약 20개의 데이터가 동적으로 추가된다면? 상상만해도 끔찍한 일이 벌어진다는것은 굳이 시도해 보지 않아도 충분히 상상할 수 있다.
다음 글에서 이런 일들이 일어나지 않게 마법처럼 도와주는 enter
함수에 대해 알아보고, 이를 이용해 간단한 bar chart를 만들어 보는 예제를 소개하겠다.