Canvas 1 Layer 1

D3.js 기초: 장대한 시각화도 select()부터
select()와 enter() 함수의 이해

D3.js 시각화가 시작되는 곳: select 함수

자바스크립트 시각화 라이브러리 D3.js에서는 조작하고자 하는 요소를 선택할 수 있는 select API를 제공합니다. select API는 제이쿼리jQueryselect API와 비슷하지만, D3.js에서는 selection 객체에 대해서 data()를 통해 특정 데이터를 바인드하고, enter()exit()를 통해 데이터에 대응하는 객체를 다룰 수 있는 기능들을 제공합니다. 이 글에서는 D3.js에서 이 selectAPI를 사용해 어떻게 시각화를 시작하는 지에 대해서 다룹니다.

D3 기초 예제 : 데이터 바인드하고 요소 추가하기

먼저 간단한 예제를 하나 살펴보겠습니다.

이 예제는 스캇 머레이의 D3.js : 쉽고 빠른 인터랙티브 데이터 시각화Interactive Data Visualization for the Web 5장에서 가져온 예제입니다. 이 예제를 말로 풀어써보자면 다음과 같습니다.

  1. body 요소를 선택하고,
  2. 그 아래에서 p요소를 전부 선택한다.
  3. dataset을 이 미리 선택한 selection 객체에 바인드한다.
  4. enter()를 통해서 p 요소에 바인드가 되지 않는, 즉 대응하는 p 요소가 없는 데이터에 대해 새로운 selection을 반환받는다.
  5. 다음으로 이렇게 선택된 요소들에 대해 실제로 p 태그로 이루어진 문서 요소를 생성한다.
  6. 이 새로운 p 요소에 “New paragraph!”라는 내용을 쓴다.

먼저 HTML 상에 <p> 요소가 하나도 없는 상태에서 이 코드를 실행했다면, 결과는 다음과 같을 것이다. 편의상 결과는 텍스트로 나타냅니다.

New panagraph!
New panagraph!
New panagraph!
New panagraph!
New panagraph!

여기서 HTML 요소를 선택하는 (1)과, (1)에서 선택된 요소 아래에서 다시 요소를 선택하는 (2)에서 하는 일은 제이쿼리와도 매우 비슷하고, 이해하기도 쉽습니다. 하지만 그 다음에 일어나는 일들은 D3에서 사용하는 고유의 데이터 처리 과정을 담고 있습니다. 이후의 과정에 대해서도 해설을 붙여보았지만, 예상컨데 D3를 따로 배워본 적이 없다면 이러한 접근은 다소 생소하게 느껴질 것입니다.

(1)~(2) D3.js select API : 시각화할 요소 선택하기

다시 하나하나의 과정을 좀 더 자세히 살펴보죠.

곰곰히 생각해보면 (1), (2)에서 하는 일이, 사실은 제이쿼리를 통해 하는 작업과 사실은 별로 비슷하지 않다는 것을 알 수 있습니다. 제이쿼리를 사용할 때는 일반적으로 이미 어떤 요소가 있다는 것을 가정하고, 그 요소를 선택하기 위해서 select API를 사용합니다. 그런데 앞서 위의 출력결과를 얻기 위해서는 <p> 요소가 하나도 없는 상태에서 이 코드를 실행했다면이라는 전제를 붙였습니다. 즉, 의도적으로 아무것도 선택하지 않았습니다.

D3.js에서는 일반적으로 메서드 체이닝 기법을 사용하는데, 이를 기반으로 생각해보겠습니다.

먼저 위 코드를 실행한 결과는 무엇을 반환할까? 개발자 도구를 통해서 이를 실행해보면 다음과 같다.

selectAll 반환 결과
selectAll 반환 결과

여기서 알 수 있다시피 실제 반환값은 배열 비슷한 무언가가 넘어옵니다. 단, 여기서 배열 안의 선택 결과 배열은 비어있습니다. 이는 엄밀히 말하면 배열이 아니라, D3.js의 selection 객체입니다. 좀 더 자세한 내용은 D3.js 소스코드에서 확인할 수 있습니다.

먼저 54-58행에 정의되어있는 d3_select() 함수는 실제로는 씨즐sizzle 라이브러리를 통해서 요소를 찾고 이를 d3_selection()으로 랩핑한 결과를 반환합니다.

d3_selectiond3_subclass 함수를 통해 객체를 확장합니다. 대부분의 기능은 /src/selection 아래의 코드를 임포트해서 구현됩니다.

이를 통해서 select 혹은 selectAll을 통해서 반환되는 결과가 d3 selection 객체라는 것을 확인할 수 있습니다. 단, 앞서 지적했듯이, 이 결과물 배열은 그 내용이 비어있다. (1)~(2)에서 하는 작업을 좀 더 쉽게 설명하자면, d3 라이브러리를 사용하기 위해 빈 selection 객체를 만드는 과정이라고 할 수 있습니다.

이에 대해서 API 문서를 확인해보겠습니다. 먼저, selectAll을 보면, selector에 매치되는 요소가 없다면 빈 selection을 반환한다고 나와있습니다.

d3.selectAll(selector)Selects all elements that match the specified selector. The elements will be selected in document traversal order (top-to-bottom). If no elements in the current document match the specified selector, returns the empty selection.

그리고 select를 살펴보면, 마찬가지로 매치되지 않으면 빈 selection을 반환한다고 합니다.

d3.select(selector)Selects the first element that matches the specified selector string, returning a single-element selection. If no elements in the current document match the specified selector, returns the empty selection. If multiple elements match the selector, only the first matching element (in document traversal order) will be selected.

여기서 하나 중요한 사실을 알 수 있습니다. 지금 살펴보고 있는 전체 예제를 다시 확인해보겠습니다.

d3.select("body")          // 1
  .selectAll("p")          // 2
  .data(dataset)           // 3
  .enter()                 // 4
  .append("p")             // 5
  .text("New paragraph!"); // 6

여기서 (1)~(2)의 과정에서 반드시 selectAll("p")를 사용할 필요는 없습니다. 여기까지 과정에서 실제로 선택되는 문서 요소는 존재하지 않기 때문에, 빈 selection 을 반환한다면 어떤 표현이라도 이를 대체할 수 있습니다. 따라서, 아래 네 표현은 모두 같습니다.

앞선 예제의 (1)~(2)를 위의 예제 코드 중 하나로 바꾸더라도, 그 결과는 같을 것입니다.* 중요한 것은 여기서 무엇을 선택했느냐가 아니라 빈 selection 객체를 시작으로 다음 작업들이 이루어진다는 점입니다.

* 좀 더 엄밀히 말하면 그 결과만 같은 것이다. 이들인 빈 selection이라는 것은 동일하지만 다른 부모 요소를 가집니다.

(3)~(4) data()와 enter() : 화면에 없는 데이터를 보여줄 준비하기

(3)~(4)는 D3.js 고유의 과정이자 핵심적인 부분이라고 할 수 있습니다. (3)에서 selection 객체에 대해서 data() 메서드를 통해 데이터를 빈 선택물에 연결지을 수 있습니다. 여기까지는 (화면 상에) 아무런 변화도 일어나지 않습니다. data()의 반환 결과에는 enter(), exit()라는 D3에서 사용하는 고유한 개념이자 메서드가 더해집니다. enter() 메서드는 selection에 바인드된 데이터들 중에 아직 실제 문서 요소를 가지지 못 하는 것들을 찾아내서 가상의 객체로 만들어 반환해줍니다.

enter() 반환결과
enter() 반환결과

여기서 알 수 있다시피, 이 객체들에는 각각의 데이터 요소들이 연결되어있습니다. (5)에서는 append()를 통해서 enter()로 생성된 가상 요소들을 빈 selection 요소의 부모 요소를 기준으로* 실제 문서 요소로 생성합니다. 여기서는 “p” 문서 요소로 생성이 되지만, p 요소는 기본적으로 보이는 내용이 없으므로 (6)에서 text() 메서드를 통해서 각 요소마다 “New paragraph!”를 보여주도록 합니다.

* 여기서는 (1)에서 선택한 body가 되거나 지정하지 않았다면 html이 될 것입니다.

여기까지가 D3.js: 장대한 시각화의 서막입니다.

보충 - select와 enter의 차이

한 가지 재미있는 사실을 짚고 넘어가죠. 이번에는 HTML에 이러한 위 예제의 자바스크립트 코드를 실행하기 전에 세 개의 p 요소가 있다고 가정해보겠습니다. body 아래의 HTML 코드는 아래와 같습니다.

이 상태에서 원래의 예제 코드를 실행시키면,

그 결과는 아래와 같습니다.

abc
abc
abc
New panagraph!
New panagraph!

분명히 데이터의 요소는 5개인데, 문단은 2개밖에 출력되지 않았습니다. 이 결과가 의아하다면 enter()를 정확히 이해하고 있지 못 하기 때문입니다. 먼저 p"요소가 하나도 없을 때 enter()의 결과를 보겠습니다.

p 요소가 없을 때 enter() 반환결과
p 요소가 없을 때 enter() 반환결과

그리고 p 요소가 3개가 있을 때 enter() 메서드의 결과를 살펴보죠.

p 요소가 이미 있을 때 enter() 반환결과
p 요소가 이미 있을 때 enter() 반환결과

앞서 이야기했다시피 enter()바인드된 데이터들 중에 아직 실제 문서 요소를 가지지 못 하는 것들을 찾아내서 가상의 객체로 만들어 반환해줍니다. 따라서, 이미 p 요소가 있을 경우 selectAll()의 결과는 더 이상 빈 selection 객체가 아니라 이미 존재하는 p 요소 3개가 선택된 상태가 됩니다. 따라서 D3.js는 우선적으로 이 요소들에 데이터가 연결되어있다고 생각하고, 나머지 아직 연결된 문서 요소가 없는 데이터에 대해서만 가상의 객체를 생성합니다. 결과적으로, 미리 존재하는 요소들은 무시됩니다.

그렇다면 이미 존재하는 요소에 대해서 enter() 메서드를 사용하면 이를 조작할 수 없다는 의미가 됩니다. 이 때는 selectAll() 이후, 혹은 data() 메서드로 데이터 바인드 이후 반환되는 결과를 바로 조작하면 됩니다.

abc
abc
abc
New panagraph!
New panagraph!

즉, 이 상태에서 데이터를 통해 문서 요소를 조작하기 위해서는 다음과 같이 할 수 있습니다.

그러면 아래와 같은 결과를 얻을 수 있을 것입니다.

5!
10!
15!
20!
25!

결론

여러 D3.js 예제들을 살펴보면 존재하지 않는 요소를 선택하고 데이터를 바인드하는 경우가 많습니다. 이런 예제를 보면 selectAllappend에서 왜 굳이 같은 요소를 사용하는 지 의문이 들 것입니다. 이 글에서는 여기에서 무슨 일이 벌어지고 있는 건지, 무엇을 선택해야하는 하는 건지에 대해서 다뤘습니다. 실제로 하는 일은 빈 selection 객체를 선택하는 일이고, 여기에 데이터를 바인드하고 바인드된 데이터에 대한 시각적 요소를 생성하는 것입니다. 이것이 빈 HTML에서 자바스크립트JavaScript만으로 시각화를 시작하는 기본적인 방법입니다. 이를 이해하고 나면 좀 더 수월하게 시각화를 시작할 수 있을 것입니다.