요즘IT
위시켓
최근 검색어
전체 삭제
최근 검색어가 없습니다.

내가 차트 라이브러리를 만든 이유

회원가입을 하면 원하는 문장을
저장할 수 있어요!

다음

회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!

확인

개발

내가 ‘차트 라이브러리’를 만들며 배운 것들

년차,
어떤 스킬
,
어떤 직무
독자들이 봤을까요?
어떤 독자들이 봤는지 궁금하다면?
로그인

내가 차트 라이브러리를 만든 이유

 

“죄송하지만, 라이브러리에서 지원하지 않는 기능이라…”

 

프론트엔드 개발자라면 누구나 한 번쯤 이런 말을 해본 적이 있을 겁니다. 특히 데이터 시각화가 중요한 프로젝트를 진행할 때면 더욱 그렇죠. 저 역시 심리 진단검사 보고서의 차트 개발을 담당했을 때 이런 말을 수없이 반복해야 했습니다.

 

“이 영역의 점수가 임계치를 넘으면 위험군으로 분류되어 붉은색으로 강조되어야 해요.”

“검사 결과를 다각형 방사형 차트로 표현하되, 각 항목별 기준선도 함께 보여주세요.”

“진단 영역별로 백분위 분포를 보여주면서, 현재 내담자의 위치도 표시해 주세요.”

 

이러한 요구사항들은 단순한 디자인 선호도가 아닌, 전문적인 진단과 해석을 위해 꼭 필요한 것들이었습니다. 처음에는 기존 차트 라이브러리의 코드를 직접 수정하고, 오버라이드하는 방식을 시도했습니다. 밤을 새워가며 라이브러리 내부 구조를 분석하고, 필요한 기능을 강제로 주입했죠. 하지만 이는 임시방편에 불과했습니다.

 

실시간 모니터링 대시보드나 금융 차트처럼, 데이터 시각화는 때로는 ‘핵심 기능’ 그 자체입니다. 심리 진단검사 결과를 시각화할 때도 마찬가지였죠. 전문가가 데이터를 직관적으로 이해하고 정확한 진단을 내리기 위해서는, 차트가 단순히 예쁜 그래프가 아닌 정확한 진단 도구로 기능해야 했습니다.

 

어떤 개발자들은 “라이브러리에서 지원하지 않으니, 기능을 조정하자”라고 타협하기도 합니다. 하지만 과연 그래도 될까요? 의료 현장의 모니터링 차트나 심리 검사 결과처럼, 전문적인 의사결정을 돕는 도구에서 개발의 편의성을 위해 사용자 경험을 희생하는 게 과연 올바른 선택일까요? 라이브러리가 업데이트될 때마다 수정한 코드들이 깨졌고, 점점 더 많은 핫픽스가 필요해졌습니다. 결국 깨달았습니다. 더 이상 기존 라이브러리의 한계에 맞춰 타협할 수는 없다고요.

 

그래서 결심했습니다. 처음부터 다시 만들기로요. 전문가들의 요구사항을 모두 수용할 수 있는, 완전히 새로운 차트 라이브러리를 만들기로 했습니다. 어떤 상황에서든 필요한 기능을 자유롭게 구현할 수 있는 도구를 만들고 싶었습니다. 이것이 제가 ‘headless-chart’를 시작하게 된 이유입니다.

 

<출처: 작가>
 

기존 라이브러리의 한계, 그리고 내가 택한 길

사실 기존 차트 라이브러리들이 나쁜 것은 아닙니다. Chart.js, D3.js, Recharts 등 수많은 라이브러리가 각자의 장점을 가지고 있죠. 하지만 실제 프로젝트에서 마주치는 디테일한 요구사항들을 구현하려 할 때면, 이 훌륭한 도구들도 종종 한계를 드러냅니다.

 

기존 라이브러리의 세 가지 근본적인 문제

가장 큰 문제는 디자인 시스템과의 충돌이었습니다. 특히 기업용 대시보드를 개발할 때는 회사의 브랜드 컬러와 UX 가이드라인을 정확히 따라야 했습니다. “이 수치는 반드시 가로로 정렬되어야 해요”, “이 라벨은 길어지면 자동으로 줄 바꿈이 되어야 해요” 같은 구체적인 요구사항들이 쏟아졌죠. 하지만 대부분의 라이브러리는 섬세한 스타일링을 완벽히 지원하지 않았습니다.

 

두 번째로는 업데이트와 호환성 문제입니다. 라이브러리가 지원하지 않는 기능을 구현하기 위해 내부 코드를 직접 수정하거나, private 메서드를 사용하다 보니 발생한 문제였죠.

 

// 커스텀 툴팁을 만들기 위해 차트의 private API를 건드립니다
const originalDraw = Chart.prototype._draw;
Chart.prototype._draw = function () {
  originalDraw.call(this);
  if (this.tooltip._active) {
    // 라이브러리 내부 구현에 의존하는 위험한 코드
    const tooltip = this.tooltip;
    const points = tooltip._active;
    // ... 커스텀 로직
  }
};

 

이런 코드는 임시방편으로는 동작할 수 있지만, 결국 시한폭탄과 같았습니다. 라이브러리가 업데이트되면서 내부 구현이 바뀌면 수정했던 코드들이 모두 깨져버렸고, 매번 새로운 버전에 맞춰 다시 고쳐야 했습니다.

 

세 번째는 복잡한 옵션 구조로 인한 유지보수의 어려움입니다. 차트의 모든 요소를 옵션으로 제어하다 보니, 코드는 점점 더 복잡해졌습니다.

 

// 기존 차트 라이브러리의 복잡한 옵션 구조
const chart = new Chart({
  options: {
    scales: {
      y: {
        ticks: {
          // 수치 가로 정렬을 위한 복잡한 옵션
          align: "end",
          crossAlign: "center",
          padding: 8,
        },
      },
    },
    plugins: {
      legend: {
        labels: {
          // 라벨 줄바꿈을 위한 해킹
          generateLabels: (chart) => {
            // 복잡한 커스텀 로직...
          },
        },
      },
    },
  },
});

 

Headless UI: 새로운 접근 방식

이러한 문제들을 해결하기 위해 도입한 것이 ‘headless UI’ 패턴입니다. headless-chart는 데이터 처리와 시각적 표현을 완전히 분리합니다. 실제 구현은 이렇게 단순해집니다.

 

const chart = BarChart({
  data, // 순수한 데이터
  custom: {
    // 각 UI 요소를 독립적으로 커스터마이징
    bar: ({ label }) =>
      Container({
        width: Infinity,
        margin: EdgeInsets.symmetric({ horizontal: 8 }),
        decoration: new BoxDecoration({
          color: getBackgroundColor(label),
          border: getBorder(label),
        }),
      }),
    // 라벨 스타일링도 자유자재
    xAxisLabel: ({ name }) =>
      Text(name, {
        style: new TextStyle({
          fontFamily: "Noto Sans JP",
          fontSize: 12,
          color: "#666666",
        }),
      }),
  },
});

 

이렇게 하면 차트의 각 요소를 마치 레고 블록처럼 조립할 수 있습니다. 데이터 처리 로직은 라이브러리가 담당하되, 시각적 표현은 완전히 자유롭게 커스터마이징할 수 있죠. 브랜드 컬러를 적용하고 싶으신가요? 간단합니다. 특별한 레이아웃이 필요하신가요? 문제없습니다. 모든 시각적 요소를 원하는 대로 구현할 수 있으니까요.

 

이것이 바로 제가 ‘headless-chart’를 시작하게 된 결정적인 계기였습니다.

 

 

headless-chart와 flitter의 탄생

차트의 본질로 돌아가기

차트의 본질적인 역할을 다시 생각해 봤습니다. 차트는 단순히 데이터를 시각화하는 것을 넘어, 사용자와 상호작용을 하며 동적으로 반응해야 합니다. 기존 차트 라이브러리들은 이런 동적인 상호작용을 구현하기 위해 복잡한 설정과 콜백 함수들을 요구했죠.

 

// 기존 방식의 복잡한 이벤트 핸들링
chart.on("mousemove", (evt) => {
  const points = chart.getElementsAtEventForMode(evt, "nearest");
  if (points.length) {
    const firstPoint = points[0];
    // 복잡한 상태 관리 코드...
  }
});

 

Flitter: 브라우저에서 영감을 얻다

이런 문제를 해결하기 위해 만든 것이 ‘flitter’입니다. 흥미롭게도, 해답은 우리가 매일 사용하는 브라우저의 동작 방식에 있었습니다. 브라우저는 어떻게 html을 가지고 복잡한 웹 페이지를 효율적으로 렌더링할까요?

 

  1. 레이아웃 계산 (Layout): 각 요소의 크기와 위치를 계산
  2. 페인팅 (Painting): 실제 픽셀을 화면에 그리기
  3. 컴포지팅 (Compositing): 레이어를 조합하여 최종 화면 구성

 

이러한 브라우저의 렌더링 프로세스에서 영감을 받아, 그리고 Flutter의 위젯 시스템을 참고하여 flitter를 설계했습니다. Flutter가 모바일 앱 개발에서 선언적 UI와 효율적인 렌더링을 제공하듯, flitter는 웹에서 같은 경험을 제공하고자 했죠.

 

// 선언적인 레이아웃 정의
const layout = Container({
  padding: EdgeInsets.only({ left: 30, bottom: 70 }),
  child: Stack({
    children: [
      // 자동으로 최적화된 레이아웃 계산
      Positioned({
        top: 0,
        right: 0,
        child: Text("Value: ${value}", {
          style: new TextStyle({
            fontFamily: "Noto Sans JP",
            fontSize: 12,
            color: "#666666",
          }),
        }),
      }),
    ],
  }),
});

// 인터랙션 정의
const interactiveChart = GestureDetector({
  onClick: (event) => {
    // 터치/클릭 이벤트 처리
  },
  onDrag: (event) => {
    // 드래그 이벤트 처리
  },
  child: chart,
});

 

선언적 제어의 진정한 힘

flitter의 가장 큰 장점은 모든 것을 선언적으로 제어할 수 있다는 점입니다.

 

1) 애니메이션: 

const animatedBar = AnimatedContainer({
  duration: 300,
  height: value, // 자동으로 부드러운 전환
  decoration: new BoxDecoration({
    color: isHovered ? "blue" : "gray",
  }),
});

 

2) 상태 관리:

class ChartWidget extends StatefulWidget {
  constructor() {
    super();
    this.state = {
      selectedBar: null,
      hoveredIndex: -1,
    };
  }

  build() {
    return BarChart({
      data,
      custom: {
        bar: ({ index }) =>
          Container({
            color: this.state.hoveredIndex === index ? "highlight" : "normal",
            child: this.buildBarContent(index),
          }),
      },
    });
  }
}

 

3) 인터랙션:

const interactiveBar = GestureDetector({
  onClick: (event) => {
    // 클릭 이벤트 처리
    this.setState({ selectedBar: event.index });
  },
  onDrag: (event) => {
    // 드래그 이벤트 처리
    this.handleChartDrag(event);
  },
  child: customBar,
});

 

렌더러 호환성과 성능 최적화

flitter는 SVG와 Canvas 모두를 지원합니다. 개발자는 동일한 코드로 두 렌더러를 모두 활용할 수 있죠.

 

// SVG 렌더러 사용
<Widget widget={chart} renderer="svg" />

// Canvas 렌더러 사용
<Widget widget={chart} renderer="canvas" />

 

 <출처: 작가>

 

특히 성능 최적화에 많은 공을 들였습니다.

 

1. Virtual DOM과 유사한 렌더 트리: 변경된 부분만 효율적으로 업데이트

2. 자동 레이아웃 최적화: 불필요한 재계산 방지

3. 레이어 관리: 애니메이션이 필요한 요소만 별도 레이어로 분리

 

이 모든 내용은 flitter.dev 문서에서 자세히 확인할 수 있습니다. flitter는 단순한 캔버스 조작 라이브러리가 아닌, 브라우저의 렌더링 파이프라인을 재해석한 결과물입니다. 리액트의 선언적 UI 관리와 브라우저의 효율적인 렌더링 프로세스, 이 두 가지 장점을 모두 취한 것이죠. 결과적으로 headless-chart는 이런 flitter의 강력한 기능을 바탕으로, 차트 개발에 있어 완전히 새로운 패러다임을 제시할 수 있게 되었습니다.

 

 

랜더링 프레임워크를 만들면서 배운 점

flitter를 만들기 전, Flutter의 소스 코드를 깊이 있게 분석했습니다. 특히 레이아웃 시스템의 설계에서 큰 통찰을 얻었습니다.

 

레이아웃의 일방향 데이터 흐름

Flutter의 가장 인상적인 부분은 복잡한 레이아웃 계산을 단순한 일방향 흐름으로 해결한 방식이었습니다.

 

// Flutter 코드 예시
class RenderObject {
  layout(Constraints constraints) {
    // 1. 부모로부터 제약조건이 아래로 전달됨
    this.constraints = constraints;

    // 2. 자식들의 레이아웃을 계산
    this.children.forEach(child => {
      child.layout(this.getChildConstraints());
    });

    // 3. 계산된 자식들의 크기를 기반으로 자신의 크기를 결정하고 위로 전달
    const size = this.calculateSize();
    return size;
  }
}

 

이 방식의 아름다움은 복잡한 부모-자식 관계의 레이아웃 계산을 단순한 한 방향 흐름으로 정리했다는 점입니다. 제약조건(Constraints)은 위에서 아래로, 크기(Size)는 아래에서 위로 전달되는 단순한 규칙으로 모든 레이아웃 계산이 이루어집니다.

 

<flutter.dev>

 

상속 대신 조합을 선택한 디자인 패턴들

Flutter 코드를 분석하면서 가장 큰 깨달음을 준 것은 ‘상속을 최소화하는 다양한 패턴들’이었습니다.

 

데코레이터 패턴: 위젯 확장의 우아한 방법

// 상속 대신 래핑으로 기능 확장
const paddedChart = Padding({
  padding: EdgeInsets.all(16),
  child: BorderContainer({
    border: Border.all({ color: 'blue' }),
    child: Chart(...)
  })
});

 

새로운 기능이 필요할 때마다 상속으로 새 클래스를 만드는 대신, 기존 위젯을 다른 위젯으로 감싸는 방식을 사용합니다. 이는 코드의 유연성과 재사용성을 크게 높여줍니다.

 

전략 패턴: 그리기 로직의 분리

특히 Border를 그리는 부분에서 전략 패턴의 실제적인 활용을 배웠습니다.

 

// Flutter의 Border 클래스에서 영감을 받은 구현
class Border {
  paint(canvas, rect) {
    // 실제 그리기 전략을 실행
    this.side.paint(canvas, rect);
  }
}

// 위젯은 단순히 전달받은 Border 객체의 paint를 호출
class BorderContainer extends Widget {
  performDraw(canvas) {
    // border 속성의 그리기 전략을 실행
    this.border.paint(canvas, this.rect);
    // 자식 위젯 그리기
    this.child?.draw(canvas);
  }
}

 

이렇게 하면 그리기 로직을 완전히 분리할 수 있고, 다른 위젯에서도 같은 Border 로직을 재사용할 수 있습니다. 상속 없이도 코드를 효과적으로 재사용하는 방법을 배운 거죠.

 

성능 최적화의 우아한 해결책

Flutter의 렌더링 최적화 기법도 많은 영감을 주었습니다. 특히 애니메이션 프레임 관리 방식이 인상적이었죠.

 

class RenderManager {
  markNeedsDraw(widget) {
    if (!this.scheduledDraw) {
      this.scheduledDraw = true;
      requestAnimationFrame(() => {
        // 한 프레임 동안 쌓인 모든 변경사항을 한 번에 처리
        this.drawDirtyWidgets();
        this.scheduledDraw = false;
      });
    }
  }
}

 

코드 분석 능력의 성장

이러한 학습 과정을 통해 얻은 가장 큰 수확은 코드를 읽는 눈이 생겼다는 것입니다. 처음에는 GOF의 디자인 패턴을 책으로 배웠을 때는 와닿지 않았던 개념들이, 구글 엔지니어들의 실제 코드를 보면서 생생하게 이해되기 시작했습니다. 이제는 어떤 라이브러리의 코드를 보더라도 “왜 이런 구조를 선택했을까?”, “이 패턴을 사용함으로써 어떤 문제를 해결했을까?”를 자연스럽게 고민하게 되었고, 그 과정에서 코드를 읽고 이해하는 능력이 크게 향상되었습니다.

 

결과적으로 이 경험들은 제가 다른 오픈소스 프로젝트들을 분석하고 기여하는 데 큰 도움이 되었습니다. 단순히 API 문서만 보고 사용하는 것이 아니라, 필요할 때 실제 코드를 파고들어 문제를 해결할 수 있는 자신감을 얻게 된 것이죠.

 

 

결론: 직접 만들어보면서 얻는 성장

headless-chart는 여전히 진화하고 있습니다. 처음 시작은 “차트를 자유롭게 커스터마이징하고 싶다”라는 단순한 필요에서 출발했지만, 이제는 더 큰 그림을 그리고 있습니다. 누구나 원하는 차트 디자인을 단숨에 만들 수 있는 라이브러리를 만드는 것이 제 목표입니다. 이를 위해서는 더 많은 사용자 피드백과 커뮤니티의 기여가 필요하고요. 이 프로젝트를 통해 제가 얻은 것은 단순히 기술적인 성과를 넘어섭니다.

 

  • 첫째, 다른 개발자의 코드를 읽고 이해하는 능력이 크게 향상되었습니다. Flutter의 코드베이스를 분석하면서, 대규모 프로젝트의 설계 철학과 패턴을 이해하는 눈이 생겼습니다.
  • 둘째, 오픈소스 생태계에 대한 이해가 깊어졌습니다. 단순히 코드를 작성하는 것을 넘어, 문서화, 이슈 관리, PR 리뷰 등 프로젝트 전반을 아우르는 경험을 쌓을 수 있었죠.
  • 셋째, 문제 해결 능력을 향상했습니다. 다른 라이브러리에서 버그를 발견했을 때도, 이제는 망설이지 않고 직접 코드를 분석하고 PR을 올립니다. "이건 어려울 거야"라는 선입견 대신 "한번 파고들어 보자"는 태도가 자연스러워졌습니다.

 

물론 지금도 매일 새로운 도전 과제들이 생깁니다. 더 나은 성능, 더 직관적인 API, 더 풍부한 기능들… 하지만 이제는 이런 도전들이 두렵지 않습니다. 오히려 설렘으로 다가오죠. 프로젝트를 시작할 때는 몰랐던 진실이 하나 있습니다. “무언가를 만든다”는 것은 단순히 결과물을 만드는 것이 아니라, 그 과정에서 자신을 성장시키는 여정이라는 것입니다. 

 

만약 이 글을 통해 관심이 생기셨다면, 부담 없이 headless-chart 깃허브에서 피드백과 아이디어를 남겨주셔도 좋습니다. 라이브러리를 만들면서 배운 가장 큰 교훈은 바로 “내가 만든 것이 누군가에게 유용해진다면, 거기서부터 모든 성장이 시작된다.”라는 사실이기 때문입니다.

 

요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.

좋아요

댓글

공유

공유

댓글 12
스벨트전도사
작가
            @lsw1991 응원 감사합니다!!
          
2025.01.08. 오후 22:59
스벨트전도사
작가
            @metis041 ㅋㅋㅋㅋㅋ
          
2025.01.08. 오후 23:00
스벨트를 전도하고 싶은 웹 개발자
6
명 알림 받는 중

작가 홈

스벨트를 전도하고 싶은 웹 개발자
6
명 알림 받는 중
스벨트전도사로 각종 커뮤니티에서 활동하고 있는 개발자 문대승입니다.

여러 오픈소스들을 만들고 운영하고 있습니다.

- 노션프레소(https://notionpresso.com)
- 노션 페이지를 쉽게 웹 사이트에 임베딩할 수 있는 툴모음(cli, renderer)
- 리액트, 스벨트 모두 지원

- 쓰고이(https://ssgoi.dev)
- 페이지 이동 애니메이션 라이브러리
- 핀터레스트 수준의 트랜지션을 크로스 브라우저 호환

- 플리터(https://flitter.dev)
- svg,canvas 모두 호환하는 랜더링 엔진 프레임워크
- 선언형으로 복잡한 데이터 시각화를 가능하게 합니다.
- flutter-like api로 학습 곡선을 낮췄습니다.
- 디비 다이어그램툴 이지알디(https://easyrd.dev) 를 만드는데 사용했습니다.

오픈소스 기여하고 싶은 분은 연락주세요

좋아요

댓글

스크랩

공유

공유

요즘IT가 PICK한 뉴스레터를 매주 목요일에 만나보세요

요즘IT가 PICK한 뉴스레터를
매주 목요일에 만나보세요

뉴스레터를 구독하려면 동의가 필요합니다.
https://auth.wishket.com/login