What is an enter() function?

December 13, 2018 · 6 mins to read

이 생각보다 팀원들의 반응이 좋았다. 간단한 글이지만 D3가 어떤 느낌인지 알 것 같다는 반응도 있었는데, 글을 쓴 결과가 어느정도 성공적이라고 생각하고 있다 생각보다 팀원들이 많이 착하다. 이번 글은 마지막에 썼듯이 정말 마법처럼 동작하는 함수 enter()와 간단한 bar chart를 그리려고 한다. 그 과정에서 chart를 그리는데 어떤 요소가 필요한지 기본적인 것을 소개하고자 한다.

먼저, prototyping을 위해서 선택할 수 있는 방법이 두 가지가 있는데, 하나는 block builder (opens new window)라는 사이트를 이용하는 방법이고, 나머지는 직접 개발환경을 구축하는 것이다. 개발환경을 구축하는 경우 npm과 node서버를 이용할 수 도 있지만, 간단하게 html 파일을 작성하는 것이 더 괜찮은 방법이라고 생각한다. 각자 즐겨 사용하는 IDE를 열고 다음과 같이 html 파일을 작성한다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>D3 example</title>
    <script src="https://d3js.org/d3.v5.min.js"></script>
  </head>
  <body>
    <script></script>
  </body>
</html>

무엇이든 준비가 되었다면, 간단하게 5개 값을 갖는 bar chart를 그려보는 것부터 시작해 보면 좋을 것 같다.

1. Drawing bar chart without enter() function

<body>
  <div class="content">
    <svg>
      <g>
        <rect></rect>
        <rect></rect>
        <rect></rect>
        <rect></rect>
        <rect></rect>
      </g>
    </svg>
    <script></script>
  </div>
</body>

먼저 위와 같은 html에 그려보는 것으로 몸을 풀어보자. data는 간단하게 const data = [1, 2, 3, 4, 5]라는 5개의 정수 배열이다.

const data = [1, 2, 3, 4, 5]
const WIDTH = 5
const HEIGHT = 100

d3.selectAll('rect') // 1
  .data(data) // 2
  .attr('x', (d, i) => i * WIDTH * 2) // 3
  .attr('y', d => HEIGHT - d * 10) // 4
  .attr('width', WIDTH)
  .attr('height', d => d * 10)
  .attr('fill', 'blue')

<script>사이에 위의 코드를 넣어보자. 귀여운 작은 사각형 5개가 나타났는가? 작은 사각형을 그리기 위해 D3은 다음과 같이 동작한다.

  1. rect태그를 모두 선택
  2. rect에 data배열에 있는 정수를 하나씩 binding해준다.
  3. x축 좌표를 정한다. 두 번째 파라미터에 함수를 넣어 이용하면 된다.
  4. y축 좌표를 정한다. 보통 svg 좌표의 경우 왼쪽 상단이 (0, 0)이고, rect는 위에서 아래로 그려지기 때문에 svg의 height에서 rect의 height를 빼 준 값이 rect의 y시작 값이 된다.
  5. 마찬가지 방법으로 width와 height가 정해지며, blue color로 rect를 채워주게 된다.

이 때 두 번째 파라미터에 들어가는 함수(d, i) => d의 파라미터를 보면, 첫 번째 d에는 data의 값들이 순차적으로 들어가고(each iteration) i에는 index가 오게 된다. 주의할 점은 select(this)등의 방법으로 자기자신의 DOM에 접근하고자 하면, arrow function이 아닌 function declaration을 이용해야 한다.

이것이 정말 D3의 강력한 점이라고 생각하는데, 간단하고 직관적인 방법으로 DOM에 binding된 데이터를 다룰 수 있는 점이 매력적이다.

2. enter()

간단하게 D3이 svg 요소를 그려주는 방법을 알았다면, enter함수를 이해하는것이 조금은 더 쉬워진다. 위에서도 계속 말한 부분이지만, 보통 이런 문제가 발생했을 경우에 쓰이게 된다.

<body>
  <div class="content">
    <svg>
      <g>
        <rect></rect>
        <rect></rect>
        ...
        <rect></rect>
        <rect></rect>
      </g>
    </svg>
    <script>
      const data = [0, 1, 2, 3, 4, ... , 599, 600]
      d3
        .selectAll('rect')
        .data(data)
        //...
    </script>
  </div>
</body>

data의 길이가 600이기때문에 rect를 600개를 html에 배치해놓았다. 이는 전혀 아름답지 않을 뿐더러, 개발자답지도 못한 해결책이다 (개발자라면 늘 이런상황에서 고민해야한다고 생각한다). 하지만 enter를 이용하면 다음과 같이 코드를 바꿀 수 있다.

<body>
  <div class="content">
    <svg>
      <g></g>
    </svg>
    <script>
      const data = [0, 1, 2, 3, 4, ... , 599, 600]
      d3.select('g')
        .selectAll('rect')
        .data(data).enter()
        .append('rect')
        //...
    </script>
  </div>
</body>

rect에 attribute를 적당히 추가해주면 600개의 rect가 생성된 것을 볼 수 있다. 이것이 바로 enter함수의 역할(MAGIC!)이다.

enter함수는 DOM에 binding되지 못한 데이터를 갖고있는, 일종의 가상요소를 모아놓은 배열이다. 확인하는 방법은 역시나 console.log를 이용하면 된다.

const data = [1, 2, 3, 4, 5]
const rects = d3.select('g').selectAll('rect')
// 1
console.log(rects)
/**
_groups: [NodeList(0)]
_parents: [g]
*/

// 2
console.log(rects.data(data))
/**
_enter: Array(1)
  0: Array(5)
    0: ot {..., __data__: 1}
    1: ot {..., __data__: 2}
    2: ot {..., __data__: 3}
    3: ot {..., __data__: 4}
    4: ot {..., __data__: 5}
    length: 5
_exit: [Array(0)]
_groups: [Array(5)]
_parents: [g]
*/

// 3
console.log(rects.data(data).enter())
/**
_enter: Array(1)
  0: Array(5)
    0: ot {..., __data__: 1}
    1: ot {..., __data__: 2}
    2: ot {..., __data__: 3}
    3: ot {..., __data__: 4}
    4: ot {..., __data__: 5}
    length: 5
*/

// 4
console.log(rects.data(data).enter().append('rect'))
/**
_groups: Array(1)
  0: Array(5)
    0: rect  // __data__: 1
    1: rect  // __data__: 2
    2: rect  // __data__: 3
    3: rect  // __data__: 4
    4: rect  // __data__: 5
*/
  1. rectselectAll했지만 실제로 DOM node가 존재하지 않아 select되지 않았다.
  2. 데이터를 binding할 경우 _enter라는 key값으로 data가 binding된 것을 확인할 수 있다.
  3. enter함수를 통해 DOM에 binding되지 못한 데이터(_enter의 value)를 반환한다.
  4. 반환된 데이터를 바탕으로 rect node를 추가되었다.

조금은 감이 오는지 모르겠다 워낙 글을 조리있게 쓰지 못하는 타입이라…. 쉽게 말하자면, enter를 통해 DOM에 binding되지 못한 데이터를 핸들링 할 수게 된다는 것이다.

그렇기때문에 미리 rect가 배치되어있지 않아도 순수하게 data를 통해 rect를 배치할 수 있게 되는 것이다. 이 말은 결국 나중에 데이터가 바뀌더라도 그에 따라 유연하게 DOM node를 컨트롤 할 수 있다는 말이 된다. 이것이 D3.js가 Data-Driven Documents라고 불리는 이유이다.

3. enter()의 역습

enter함수에 대해서 한가지 조심해야할 부분은, 위에서도 강조했지만 enter함수는 DOM에 binding되지 못한 데이터만을 반환한다는 것이다. 개인적인 경험에 의해서 이 부분은 꼭 이해하고 넘어갔으면 좋겠다는 바람이 있어 굳이 섹션을 나누게 되었다.

아래의 예를 보자.

<!-- before -->
<body>
  <div class="content">
    <p>Test</p>
    <p>Test</p>
    <script>
      const data = [1, 2, 3, 4, 5]
      d3.select('div.content')
        .selectAll('p') // 1
        .data(data) // 2
        .enter() // 3
        .append('p')
        .text(d => d) // 4
    </script>
  </div>
</body>

<!-- after -->
<body>
  <div class="content">
    <p>Test</p>
    <p>Test</p>
    <p>3</p>
    <p>4</p>
    <p>5</p>
  </div>
</body>

여기서 부터 슬슬 헷갈리기 시작할 것인데 이것이 이해가 가지 않는다면 enter를 제대로 이해하지 못한 것이다. enter는 DOM에 binding되지 못한 데이터만 반환하기 때문에 다음과 같이 동작한다고 생각하면 될 것이다.

  1. p node를 모두 select한다.
  2. 기존에 존재하는 p node에 0, 1을 바인딩하고(2개 이므로), 나머지는 _enter의 value로 설정한다.
  3. enter함수가 실행된 결과, DOM에 binding되지 못한(_enter의 value로 설정된) [3, 4, 5]가 반환된다.
  4. 반환된 [3, 4, 5]만 p태그 안에 감싸져 DOM에 나타난다.

위의 예제가 ’DOM에 binding되지 못한 데이터만을 반환한다’는 말을 이해를 하는데 도움이 되었으면 좋겠다. 아마 별다른 설명이 필요 없을것이라고 생각한다.

enter함수는 정말 중요하고, 그 만큼 많이 쓰인다. 특히 나중에 데이터를 동적으로 변경하기 위해 enter-update-exit 패턴을 이해하기 위해서 조금은 어렵더라도 꼭 이해하였으면 좋겠다.

다음 글에서는 본격적으로 chart를 chart답게 만들어주는 여러 기능에 대해 소개하고, 그럴듯해 보이는 chart를 만들어 보는 예제를 소개하겠다.


참고


© 2021, Built with Gatsby