리액트에서 DOM 에 직접적인 접근을 할 때, ref
ref 알아보기
리액트 개발을 하다보면 DOM 에 직접적인 접근을 해야 할 때가 있습니다. 그럴 때는 ref 라는것을 사용합니다. 그런데 정확히 어떠한 상황에 DOM 에 직접적인 접근이 필요할까요? 필요한 상황은 다음과 같습니다.
- input / textarea 등에 포커스를 해야 할때
- 특정 DOM 의 크기를 가져와야 할 때
- 특정 DOM 에서 스크롤 위치를 가져오거나 설정을 해야 할 때
- 외부 라이브러리 (플레이어, 차트, 캐로절 등) 을 사용 할 때
등이 있습니다. 이 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 라는 차트 라이브러리를 사용하는 예제를 살펴보겠습니다.
외부라이브러리를 사용 할 때에는 다음 사항들을 기억해주세요.
- 라이브러리를 적용 할 DOM 에 ref 를 설정합니다.
- componentDidMount 가 발생하면 외부라이브러리 인스턴스를 생성합니다.
- 컴포넌트 내용이 바뀌어야 할 일이 있다면, componentDidUpdate 에서 기존의 인스턴스를 제거하고 새로 인스턴스를 만듭니다.
- 이 과정에서, componentDidUpdate 에서 실제 데이터가 바뀌었는지 비교를 해야합니다.
- 컴포넌트가 언마운트될 때 인스턴스를 제거합니다.
이 튜토리얼은 하나하나 직접 진행하지는 않고, 만들어진 미니 프로젝트의 코드를 확인해보겠습니다. 코드 보기
이 프로젝트에는 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();
}
}
외부 라이브러리를 사용 할 땐, 이 흐름만 기억해 두세요!