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


ref 알아보기

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

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

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

CodeSandbox: https://codesandbox.io/s/84z0woj45j

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 에서 실제 데이터가 바뀌었는지 비교를 해야합니다.
  1. 컴포넌트가 언마운트될 때 인스턴스를 제거합니다.

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

이 프로젝트에는 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();
    }
  }

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

  • hazebob

    좋은 강좌 써주셔서 감사합니다. 설명을 참 잘 해주시고 예제도 적당하여 참 잘 보고 있습니다.
    한 가지 궁금한 건 리액트를 이정도로 공부하신 경로나 참고 자료가 있으신지요? 있다면 공유해주실 수 있을까요?
    리액트 관련하여 수많은 자료가 범람하지만 이렇게 정리가 잘 된 강좌는 없어서요..^^;

    • 강좌의 주제의 경우 http://www.tutorialspoint.com/reactjs/index.htm TutorialsPoint 에 기반해서 작성하고 있습니다. 그리고 매뉴얼이랑 여기저기 구글링해서 작성하였구요!

      • hazebob

        답변 감사합니다. 🙂 좋은 강의 잘 보고 배우겠습니다.

  • GT M

    react같은 것들은 실무에서 어떻게 쓰이나요??

    • Woosung Chu

      Jquery-tmpl.js , Handlebar.js 이러한 것들과 용도가 비슷합니다. Js MVC framework와는 다르죠

  • Junho Park

    강좌에 있는 방식이 삭제될 예정이라고 합니다.

    ref={(input) => { this.textInput = input; }} />

    이런식으로 callback 방식으로 변경해서 사용해야 합니다.

    https://facebook.github.io/react/docs/refs-and-the-dom.html#legacy-api-string-refs

  • jyblues

    Parent Component에서 Child Component에 method를 호출할려면 어떻게 하면 될까요?

    • Wonkun Kim

      parent 에서 child로 넘어가는 props를 변경하고 child 의 componentWillReceiveProps에서 child의 method를 호출하면 가능할 것 같은데요.

      예)

      Parent:

      Child
      {
      componentWillReceiveProps(nextProps) {
      if (nextProps.action === ‘callFuncA’)
      this.callFuncA()
      }

      callFuncA () {
      console.log(‘func A is called”
      }
      }

  • 잘 몰라서 질문드리는데요..
    멤버변수 설정 시 생성자에서 정의하지 않은 이유가 있나요?
    클래스에서 저렇게 바로 this에 정의하면 오브젝트 특성상 프로퍼티로 정의는 되는데
    w3에서는 권장하지 않는다고 하더군요…