리액트에서 DOM 에 직접적인 접근을 할 때, ref

ref 알아보기

리액트 개발을 하다보면 DOM 에 직접적인 접근을 해야 할 때가 있습니다. 그럴 때는 ref 라는것을 사용합니다. 그런데 정확히 어떠한 상황에 DOM 에 직접적인 접근이 필요할까요? 필요한 상황은 다음과 같습니다.

  1. input / textarea 등에 포커스를 해야 할때
  2. 특정 DOM 의 크기를 가져와야 할 때
  3. 특정 DOM 에서 스크롤 위치를 가져오거나 설정을 해야 할 때
  4. 외부 라이브러리 (플레이어, 차트, 캐로절 등) 을 사용 할 때

등이 있습니다. 이 ref 를 사용하는 예제를 한번 봐볼까요?

CodeSandbox: https://codesandbox.io/s/24l52wq47j

import React, { Component } from "react";

class RefSample extends Component {
  state = {
    height: 0
  };

  input = null;
  box = null;

  handleClick = () => {
    this.input.focus();
  };

  componentDidMount() {
    this.setState({
      height: this.box.clientHeight
    });
  }

  render() {
    return (
      <div>
        <input
          ref={ref => {
            this.input = ref;
          }}
        />
        <button onClick={this.handleClick}>Focus Input</button>
        <div
          ref={ref => {
            this.box = ref;
          }}
        >
          <h2>TITLE</h2>
          <p>Content</p>
        </div>
        <p>
          <b>height:</b> {this.state.height}
        </p>
      </div>
    );
  }
}

export default RefSample;

ref 를 설정할땐 다음과 같이 DOM에 ref 속성을 설정합니다. 설정하는 값은 함수인데요, ref 를 파라미터로 가져와서 여기서 컴포넌트의 멤버변수로 설정하면 됩니다.

<div ref={ref => { this.mydiv = ref }}></div>

ref 의 이름은 여러분이 마음대로 정하셔도 됩니다.

외부 라이브러리 사용

Chart.js 라는 차트 라이브러리를 사용하는 예제를 살펴보겠습니다.

외부라이브러리를 사용 할 때에는 다음 사항들을 기억해주세요.

  1. 라이브러리를 적용 할 DOM 에 ref 를 설정합니다.
  2. componentDidMount 가 발생하면 외부라이브러리 인스턴스를 생성합니다.
  3. 컴포넌트 내용이 바뀌어야 할 일이 있다면, componentDidUpdate 에서 기존의 인스턴스를 제거하고 새로 인스턴스를 만듭니다.
    • 이 과정에서, componentDidUpdate 에서 실제 데이터가 바뀌었는지 비교를 해야합니다.
  4. 컴포넌트가 언마운트될 때 인스턴스를 제거합니다.

이 튜토리얼은 하나하나 직접 진행하지는 않고, 만들어진 미니 프로젝트의 코드를 확인해보겠습니다. 코드 보기

이 프로젝트에는 3개의 컴포넌트가 있는데, 우선 App 부터 살펴볼까요?

App.js

import React, { Component } from 'react';
import moment from 'moment';
import './App.css';
import axios from 'axios';
import Buttons from './components/Buttons';
import LineChart from './components/LineChart';

class App extends Component {

  state = {
    pair: 'BTCUSD',
    data: []
  }

  handleChangePair = (pair) => {
    // pair 값을 바꾸는 함수
    this.setState({ pair });
  }

  getData = async () => {
    const { pair } = this.state;
    try {
      // API 호출하고
      const response = await axios.get(`https://api.bitfinex.com/v2/candles/trade:5m:t${pair}/hist?limit=288`);
      // 데이터는 다음과 같은 형식인데,
      /* [ MTS, OPEN, CLOSE, HIGH, LOW, VOLUME ] */
      const data = response.data.map(
        // 필요한 값만 추출해서 날짜, 값이 들어있는 객체 생성
        (candle) => ({
          date: moment(candle[0]).format('LT'), // 시간만 나타나도록 설정
          value: candle[2]
        })
      ).reverse(); // 역순으로 받아오게 되므로 순서를 반대로 소팅
      this.setState({
        data
      });  
    } catch (e) {
      console.log(e);
    }
  }

  componentDidMount() {
    // 첫 로딩시에 getData 호출
    this.getData();
  }

  componentDidUpdate(prevProps, prevState) {
    // pair 값이 바뀌면, getData 호출
    if (prevState.pair !== this.state.pair) {
      this.getData();
    }
  }


  render() {
    return (
      <div className="App">
        <Buttons onChangePair={this.handleChangePair} />
        { /* 데이터가 없으면 렌더링하지 않음 */ }
        { this.state.data.length > 0 && <LineChart data={this.state.data} pair={this.state.pair}/> }
      </div>
    );
  }
}

export default App;

이 컴포넌트에서는, getData 라는 함수가 있는데요, axios 를 통해서 bitfinex 의 차트 API 를 호출해옵니다. 그리고, 어떤 암호화폐의 차트 정보를 들고올지, state 에서 값을 받아와서 결정합니다.

차트 데이터를 받아와서, 우리가 만들 LineChart 컴포넌트에 전달해주겠습니다. 각 함수에 주석도 잘 설정되어있으니 하나 하나 읽어보세요.

Buttons.js

import React from 'react';
import './Buttons.css';

const pairs = ['BTCUSD', 'ETHUSD', 'XRPUSD'];

const Buttons = ({ onChangePair }) => {
  const buttonList = pairs.map(
    pair => (<button key={pair} onClick={() => onChangePair(pair)}>{pair}</button>)
  );

  return (
    <div className="Buttons">
      {
        buttonList
      }
    </div>
  );
};

export default Buttons;

Buttons 컴포넌트는 너무나 간단한 컴폰너트입니다. 그냥 pairs 로 받아온 값을 가지고 3개의 버튼을 보여주고, 클릭됨에 따라 onChangePair 를 호출하죠

LineChart.js

이 튜토리얼에서 가장 중요한 컴포넌트인 LineChart 를 봅시다.

import React, { Component } from "react";
import Chart from "chart.js";
import "./LineChart.css";

class LineChart extends Component {

  chart = null;

  draw() {
    // 새로 그려질 때 기존 인스턴스 제거
    if (this.chart) {
      this.chart.destroy();
      this.chart = null;
    }

    const { data, pair } = this.props;

    const config = {
      type: "line",
      data: {
        labels: data.map(d => d.date),
        datasets: [
          {
            label: "price",
            data: data.map(d => d.value),
            fill: false,
            backgroundColor: 'blue',
            borderColor: 'blue',
            lineTension: 0,
            pointRadius: 0,
          }
        ]
      },
      options: {
        responsive: true,
        title: {
          display: true,
          text: `${pair} 24hr Chart`
        },
        tooltips: {
          mode: "index",
          intersect: false
        },
        hover: {
          mode: "nearest",
          intersect: true
        }
      }
    };

    const ctx = this.canvas.getContext("2d");
    this.chart = new Chart(ctx, config);
  }

  componentDidMount() {
    this.draw();
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevProps.data !== this.props.data) {
      this.draw();
    }
  }

  componentWillUnmount() {
    // 컴포넌트가 사라질 때 인스턴스 제거
    if (this.chart) {
      this.chart.destroy();
    }
  }


  render() {
    return (
      <div className="LineChart">
        {/* 
          ref 를 통해서 실제 DOM 에 대한 접근
        */}
        <canvas ref={ref => (this.canvas = ref)} />
      </div>
    );
  }
}

export default LineChart;

보시면, render 에서 ref 설정이 되었죠?

 <canvas ref={ref => (this.canvas = ref)} />

그리고, 차트 인스턴스를 생성하는 draw 라는 함수를 만들었고, 컴포넌트가 마운트 될 때 호출했습니다.

  componentDidMount() {
    this.draw();
  }

그리고, 업데이트될 때도 조건부로 인스턴스를 제거시키고 새로 생성해주었습니다.

  componentDidUpdate(prevProps, prevState) {
    if (prevProps.data !== this.props.data) {
      this.draw();
    }
  }

마지막으로 컴포넌트가 언마운트 될 때 (물론 지금의 프로젝트에선 언마운트 되는 상황이 없지만) 차트 인스턴스를 제거했습니다.

  componentWillUnmount() {
    // 컴포넌트가 사라질 때 인스턴스 제거
    if (this.chart) {
      this.chart.destroy();
    }
  }

외부 라이브러리를 사용 할 땐, 이 흐름만 기억해 두세요!

results matching ""

    No results matching ""