[좌충우돌 개발기] 앗! 리액트 + 하이차트로 차트를 그렸는데 차트가 갱신이 안되네!?

안녕하세요, 플랫폼별 QR, 바코드 스캐너 구현기를 썼던 이동훈입니다.

최근 대시보드 화면 구현 중 기본을 놓쳐서 시간을 낭비한 경험이 있었습니다. 이번 글에서는 삽질기를 돌아보고,  React + Highchart 조합을 사용하여 대시보드 구성시 주의해야 할 데이터 초기 로딩 방법데이터 변경 시 차트 리렌더링은 어떻게 해야 하는지에 관하여 이야기해보려 합니다.

 Highchart

구현에 사용한 기술 중 Highchart에 관해서 위키를 통해 잠시 살펴보면

Highcharts is a software library for charting written in pure JavaScript, first released in 2009.

pure라는 단어와 2009년에 처음 배포가 되었다는 사실이 눈에 들어옵니다.

 구현해야 할 사항들

  • 로그인 시 첫 화면에 당일 기준 매출 금액, 매출 건수, 반품 금액, 반품 건수를 보여주는 차트를 구현한다.
  • 1일, 1주, 1달, 1년의 기간을 설정할 수 있는 버튼이 존재하고 버튼을 눌렀을 때, 데이터가 갱신되고 차트도 갱신된다.
  • 고객 데모시연용 화면임
    • 데모까지 2일, 구현 후 내부 확인 까지 1일의 기간이 주어짐.

 삽질기 소개

최대한 빠르게 React + Highchart 사용 예제를 찾아서 확인한 후, 원하는 모양의 정적 json을 차트 컴포넌트에 바인딩 해 보았습니다.

나: 잘 나오네 ㅎㅎ React + Highchart 짱짱!  API만 만들어서 붙이면 되겠다~

API를 만들고 차트 데이터를 state 변수에 바인딩 한 후, state 가 바뀔 때, 차트도 바뀌길 기대하며 확인해 보았습니다. 하지만... 데이터만 갱신이 되고, 차트는 갱신이 되지 않았습니다.

나: (이상하다...고뇌, 분명 state 변수는 갱신이 되는데, 왜 차트가 안 바뀌지???)

잠시 삽질후..

나: (빨리 돌아가는 화면이 나와야 하는데...) 김이사님 도와주세요~
김: 여기(사내 다른 프로젝트 코드) 보고 하세요.
나: 아..잘되네요 ㅎㅎ(일단구현Screen Shot 2019-05-21 at 23.06.17)

 구현시 겪었던 문제분석

코드분석

시간에 쫓겨 코드를 잘 살피지 못했었기 때문에 주말을 이용해 코드를 다시 살펴보았습니다.

문제는 copy & paste 했던 코드

1
2
3
4
5
6
7
8
...
componentDidMount() {
    this.chart = new Highcharts[this.props.type || 'Chart'](
      this.chartContainer.current,
      this.props.options
    );
}
...

HighCharts React Official 코드를 비교해 보니 쉽게 알 수 있었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
componentDidMount() {
    const props = this.props;
    const highcharts = props.highcharts || window.Highcharts;
    // Create chart
    this.chart = highcharts[props.constructorType || "chart"](
      this.container.current,
      props.options,
      props.callback ? props.callback : undefined
    );
}
componentDidUpdate() {
    if (this.props.allowChartUpdate !== false) {
      this.chart.update(
        this.props.options,
        ...(this.props.updateArgs || [true, true])
      );
    }
}
...

갱신이 안되었던 코드에는 update 로직이 없었던 것이었습니다.  하지만 조금 이상합니다. "React는 원래 부모의 state를 갱신하면 자식의 render 도 수행되는 것 아닌가?" 하는 의문이 드는데요. 아래 내용과 연관이 있습니다.

Dom 바인딩

HighChart는 차트 생성 시 container라고 부르는 Dom component 가 필요한데요.

1
2
this.chart = Highcharts.chart('chart', this.state.options); 
<div id="chart"></div>

위와 같은 코드입니다. 이 부분을 통해 HighChart는 id를 참조하여 차트가 그려질 공간에 바인딩 되는 것을 알 수 있습니다. 여기서 드디어 첫 코드에서는 state 가 바뀌었는데 차트가 다시 그려지지 않은 실마리를 찾을 수 있었습니다. HighChart 가 차트를 그리는 방법은 그려질 위치, 차트에 관한 옵션 를 인자로 받아 pure 자바스크립트로 된 차트 객체를 생성하여 dom에 바인딩 하는 방식으로 차트를 그리고 있었습니다. 그런데, 첫 번째 코드에서는 componentDidMount에서 1회만 차트 객체가 생성되기 때문에 state 가 변경이 되어도 차트가 갱신되지 않는 것이었습니다.

2번째 코드에서 차트의 갱신 원리를 그림과 함께 정리해보면 아래와 같습니다.

  • 부모 컴포넌트의 state에 차트 데이터에 해당하는 options가 할당이 되어 있습니다.
  • API를 통해 options의 데이터가 갱신이 완료되면, 자식 컴포넌트의  componentDidUpdate가 실행됩니다.
  • componentDidUpdate 내부에선 HighChart 가 제공하는 update 함수를 이용하여 차트를 갱신합니다.

Screen Shot 2019-05-22 at 01.54.17

주목할 점은 차트를 갱신하는데 React에서 제공하는 state 변수를 사용한 것이 아니라 this.chart.update 즉, pure javascript HighChart에서 제공하는 update 함수를 사용했다는 점입니다. (왜 react인데 react처럼 쓰지를 않는 거니..Screen Shot 2019-05-21 at 23.06.17) (참고: HighChart Official Blog)

차트 갱신시 State 는 사용하지 못하는 건가?

이쯤에서 제가 해결했던 코드를 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class App extends Component {
  constructor(props) {
    super(props);
    this.chart = "";
    this.state = {
      options: {
        ...
    }
  }
  componentDidMount() {
    this.loadChartData();
  };
  loadChartData = () => {
    this.setState({options: {...this.state.options, series: [{
        ...
      },
    ]}}, () => this.chart = Highcharts.chart('chart', this.state.options));
  };
...
  render() {
    return (
      <div className="App">
        <div>
          <button onClick={this.loadChartData}>change</button>
        </div>
        {this.state.loading
            ? (<div>Loading...</div>)
            : (<div id="chart"></div>)}
      </div>
    );
  }
}
export default App;

위 코드를 보면 componentDidMount에서 초기 데이터를 호출했고, event 함수(onClick)에 데이터 갱신 함수(loadChartData)를 바인딩 하여 차트를 갱신했습니다.

1
this.chart = Highcharts.chart('chart', this.state.options)

꺼림칙한 부분은 위 코드인데요, 데이터가 갱신될 때마다, this.chart 변수에 새로운 Chart 객체가 할당되는 것을 볼 수 있습니다.  여기까지 정리해 보고 나니 현재 코드의 개선점을 찾은 것 같습니다.

개선할 점은 차트를 매번 새로 생성하여 할당하는 부분을 HighChart에서 제공하는 update  함수를 사용하여 아래와 같이 수정하는 것입니다.

1
2
3
4
5
6
loadChartData = () => {
    this.setState({options: {...this.state.options, series: [{
        ...
      },
    ]}}, () => this.chart.update(this.state.options));
  };

정리

데이터 초기 로딩은 어디서 해야 하나?

  • componentDidMount에서 하면 됩니다.  (주의할 점: 초기 데이터 로딩전 HighChart 객체를 생성하여 Dom에 바인딩 해주어야 합니다.)

데이터 변경 시 차트 리렌더링은 어떻게 하나?

  • 차트 데이터는 state에 바인딩 한다.
  • Highart 가 제공하는 update  함수를 사용하여 차트를 갱신
    • 부모 + 자식 컴포넌트 방식을 사용했다면, 자식 컴포넌트의 componentDidUpdate 안에서 호출.
    • 1개의 컴포넌트에서 사용했다면, event 함수 안에서 먼저 setState를 통해 차트 데이터를 갱신하고, setState의 콜백에서 호출.

생각해보면 React  Life Cycle에 대한 동작원리와 HighChart 가 React 환경에서 어떻게 동작하는지 조금만 생각해 보았으면 삽질을 안 했을 수도 있었을 것 같습니다. 기본에 충실해야 하는 것을 잊지 말고자 회고 성격의 글을 작성해보았습니다. 읽어주셔서 감사합니다.


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.