Posts [React] ref란? - DOM에 직접 접근하기(useRef)
Post
Cancel

[React] ref란? - DOM에 직접 접근하기(useRef)


ref란? - DOM에 직접 접근하기


🔍 DOM요소에 이름을 달아 직접 접근할 때

HTML을 작성할 때, div 등의 DOM 요소에 이름을 달 경우 <div id="my-id"> 와 같이 id를 사용합니다.

이렇게 하면 특정한 id에 해당하는 DOM 요소에만 스타일을 따로 적용하거나, 자바스크립트에서 해당 DOM 요소에 접근하여 여러 가지 작업을 할 수 있습니다.

HTML을 작성할 때 이렇게 id를 붙이는 것처럼, 리액트에서도 DOM을 선택해 직접 접근하기 위해 ref를 사용합니다.


🔍 어떤 작업에서 ref를 사용할까?

DOM 요소에 특정한 작업을 할 경우 ref 를 사용해야 하겠지만, 어떤 작업을 할 때 특히 사용하게 될까요?

React에서 state로만 해결할 수 없고, DOM을 반드시 직접 건드려야 할 때 사용하게 됩니다.

  • 예: 특정 input에 focus 주기, 스크롤 박스 조작, Canvas 요소에 그림 그리기 등…

✏️ 함수형 컴포넌트 - useRef를 사용하여 ref 달기

함수형 컴포넌트에서는 useRef라는 hook을 사용하여 ref를 활용하게 됩니다.

아래 예시는 dom-to-image 라는 라이브러리를(특정 DOM 요소를 png로 바꿔 저장시켜줌) 사용하기 위해,

원하는 요소에 ref를 부여하여 코드를 작성한 것입니다.

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
import { useRef } from 'react';
import domtoimage from 'dom-to-image'

...
const TestComponent = (props) => {

    const {id, nickName} = props;
    const personInfo = useRef();

    // 영역을 png로 저장하는 함수
    const onClick = function () {
        domtoimage.toPng(personInfo.current) // ref를 부여한 요소에 .current로 접근
        .then(function (blob) {
          window.saveAs(blob, 'user-card.png');
        })
    };

    return (
        <>
            <div className="box" ref={personInfo}> {/*div.box 요소에 ref 부여함*/}
                <p> {id} - {nickName} 테스트 영역 </p>
            </div>
            <button onClick={onClick}>저장</button>
        </>
    );
}

먼저, const personInfo = useRef(); 를 통해 personInfo 에 ref 객체를 반환합니다.

그리고 class명이 box인 div 요소에 ref={personInfo} 값을 부여하였습니다.

이후 personInfo.current를 하면, 위에서 부여한 div 요소에 접근 가능한 id처럼 사용할 수 있습니다. (personInfo.current는 이 div 요소를 가리킵니다.)

위 용도 외에도 useRef값이 변해도 리렌더링하지 않도록 하는 변수를 관리하는 용도로 사용할 수도 있습니다. 하지만 이 용도는 이 포스팅에서는 다루지 않고 다른 포스팅에서 포함할 예정입니다.


✏️ 클래스형 컴포넌트 - 콜백 함수를 통한 ref 설정하기

위에서는 함수형 컴포넌트에서 ref를 사용하는 방법을 알아보았습니다.

리액트 클래스형 컴포넌트에서 ref를 사용하는 방법은 다음과 같습니다.

ref를 달고자 하는 요소에, ref라는 콜백 함수를 props로 전달해 주면 됩니다. 이 콜백 함수는 ref 값을 파라미터로 전달받으며, 함수 내부에서 파라미터로 받은 ref를 컴포넌트의 멤버 변수로 설정합니다.

자세한 사용 형태는 아래와 같습니다.

1
2
3
4
5
<input
  ref={(ref) => {
    this.input = ref;
  }}
/>

위의 input 요소는 앞으로 리액트 코드 내에서 this.input을 통하여 접근이 가능해집니다.

this.input 말고 사용자가 원하는 어떤 이름이라도 가능합니다. this.inputName 이라 해도 아무 상관이 없습니다. HTML에서 id를 지어주는 것과 마찬가지입니다.

즉, 리액트 내에서 id 처럼 쓰이게 된 것입니다.

위처럼 이름을 지어주고 난 뒤 어떻게 활용하는지 아래 코드를 참고하시면 됩니다.

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
34
35
36
37
38
39
40
41
42
43
import React, { Component } from "react";
import "./ValidationSample.css";

class ValidationSample extends Component {
  state = {
    password: "",
    clicked: false,
    validated: false,
  };

  handleChange = (e) => {
    this.setState({
      password: e.target.value,
    });
  };

  // 버튼 클릭 이벤트 핸들러 함수
  handleButtonClick = () => {
    this.setState({
      clicked: true,
      validated: this.state.password === "0000",
    });

    this.inputName.focus(); // ref를 통해서 아래 input 요소에 접근
  };

  render() {
    return (
      <div>
        <input
          ref={(ref) => (this.inputName = ref)}
          type="password"
          value={this.state.password}
          onChange={this.handleChange}
          className={this.state.clicked ? (this.state.validated ? "success" : "failure") : ""}
        />
        <button onClick={this.handleButtonClick}>검증하기</button>
      </div>
    );
  }
}

export default ValidationSample;

보시면 input 요소에 this.inputName 이라고 ref를 달았습니다. 이제, 리액트 내부에서 this.inputName 을 통해 이 DOM 요소에 작업을 할 수 있습니다.

버튼 클릭 이벤트 핸들러 함수 handleButtonClick 함수 마지막 부분을 보면, this.inputName.focus(); 코드가 있습니다. 즉, 버튼을 클릭하는 동시에 input 요소에 포커스(커서)가 가도록 설정한 부분입니다.

이를 설정할 때 마치 HTML 내부의 자바스크립트에서 id로 접근하듯이 지정한 ref인 this.inputName 으로 .focus()를 설정해 주었습니다.


✏️ 클래스형 컴포넌트 - 컴포넌트에 ref 달기

지금까지는 ref를 DOM 요소에 다는 것을 살펴보았는데, 리액트는 컴포넌트에도 ref를 달 수 있습니다. 이 방법은, 컴포넌트 내부의 DOM 요소를 컴포넌트 외부에서 사용해야 할 때 활용하게 됩니다.

ref를 다는 방법은 DOM 요소에 다는 방법과 동일합니다.

1
2
3
4
5
<MyComponent
  ref={(ref) => {
    this.myComponent = ref;
  }}
/>

이렇게 할 경우 컴포넌트 외부에서 myComponent.handleClick, myComponent.input 과 같이 MyComponent 내부의 메서드나 멤버변수 등에 접근이 가능해집니다.

다음은 스크롤 박스 컴포넌트를 만들고, 컴포넌트에 ref를 단 뒤, 컴포넌트 내부 메서드를 호출하는 예제입니다.

  • ScrollBox.js
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
34
35
36
37
import React, { Component } from "react";

class ScrollBox extends Component {
  ScrollToBottom = () => {
    const { scrollHeight, clientHeight } = this.box;
    this.box.scrollTop = scrollHeight - clientHeight;
  };

  render() {
    const style = {
      border: "1px solid black",
      height: "300px",
      width: "300px",
      overflow: "auto",
      position: "relative",
    };

    const innerStyle = {
      width: "100%",
      height: "650px",
      background: "linear-gradient(white, black)",
    };

    return (
      <div
        style={style}
        ref={(ref) => {
          this.box = ref;
        }}
      >
        <div style={innerStyle}></div>
      </div>
    );
  }
}

export default ScrollBox;
  • App.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { Component } from "react";
import "./App.css";
import ScrollBox from "./ScrollBox";

class App extends Component {
  render() {
    return (
      <div>
        <ScrollBox ref={(ref) => (this.scrollBox = ref)} />
        <button onClick={() => this.scrollBox.ScrollToBottom()}>맨 밑으로</button>
      </div>
    );
  }
}

export default App;

App.js 에서 ScrollBox 컴포넌트에 ref를 달았고, 그 밑에 button에서 this.scrollBox.ScrollToBottom() 과 같이 내부 메서드에 접근한 것을 확인할 수 있습니다.

1
<button onClick={this.scrollBox.ScrollToBottom()}>

위와 같이 써도 문법 에러는 아니지만, 컴포넌트가 처음 렌더링될 때에는 아직 ScrollBox 컴포넌트에 ref가 달리기 전이므로 this.scrollBox 자체가 undefined 입니다. 따라서 this.scrollBox.ScrollToBottom() 를 실행하는 과정에서 오류가 발생합니다.

이 때, 위의 App.js 코드에 작성한 것과 같이 button의 onClick 에 화살표 함수 문법으로 아예 새로운 함수를 만들고, 그 함수 내부에서 메서드를 실행하는 것으로 구현하게 된다면, 버튼을 누를 때 그 값을 읽어서 실행합니다.

즉, 버튼을 누를 때는 이미 this.scrollBox를 위에서 설정한 이후이므로 this.scrollBox가 undefined가 아니며, 오류가 발생하지 않습니다.


이 포스팅의 일부는 리액트를 다루는 기술(저자 김민준)을 학습하는 과정에서 개인적으로 정리한 내용입니다.