리액트 - ref: DOM에 이름 달기

호박이와 칼림바·2023년 9월 21일
0

React

목록 보기
6/16
post-thumbnail

📗목차

ref

public/index.html

<html>
  <head>
    (...) // 생략
  </head>
  <body>
    <div id="root>
  </body>
</html>

이렇게 HTML에서 id를 사용하여 DOM에 이름 다는 것처럼 리액트 프로젝트 내부에서 DOM에 이름을 다는 방법이 있다. 그것은 ref(reference의 줄임말) 개념이다.

💭 리액트 컴포넌트 안에서 id를 사용하면 안 되나?

리액트 컴포넌트 안에서도 id를 사용할 수는 있다. JSX 안에서 DOM에 id를 달면 해당 DOM을 렌더링할 때 그대로 전달된다. 하지만 특수한 경우가 아니면 사용을 권하지 않는다. 예를 들어 같은 컴포넌트를 여러 번 사용한다고 가정해보라. HTML에서 DOM의 id는 유일해야 하는데, 이런 상황에서는 중복 id를 가진 DOM이 여러 개 생기니 잘못된 사용이다.

ref는 전역적으로 작동하지 않고 컴포넌트 내부에서만 작동하기 때문에 이런 문제가 생기지 않는다.

ref는 어떤 상황에서 사용해야 될까?

DOM을 꼭 직접적으로 건드려야 할 때 ref를 사용해야 한다.
예를 들어 일반 순수 자바스크립트 및 jQuery로 만든 웹 사이트에서 input을 검증할 때는 다음과 같이 특정 id를 가진 input에 클래스를 설정해 준다.

일반 HTML 예제 코드

<html>
<head>
  (...) // 생략
  <style>
    .success { background-color: green; }
    .failure { background-color: red; }
  </style>
  <script>
    function validate() {
      var input = document.getElementById('password');
      input.className = '';
      if(input.value === '0000') {
        input.className = 'success';
      } else {
        input.className = 'failure';
      }
    }
</head>
<body>
  <input type="password" id="password"></input>
  <button onclick="validate()">Validate</button>
</body>
</html>   

하지만 리액트에서 이런 작업은 굳이 DOM에 접근하지 않아도 state로 구현할 수 있다. 다음 코드를 살펴보자.

ValidationSample.css

.success { background-color: lightgreen; }
.failure { background-color: lightcoral; } 
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'
    });
  }
    
  render() {
    return (
      <div>
        <input
          type="text"
      	  value={this.state.password}
		  onChange={this.handleChange}
		  className={this.state.clicked ? (this.state.validated ? 'success' : 'failure') : ''}
		/>
        <button onClick={this.handleButtonClick}></button>
	  </div>
    );
  }
}
import React from 'react';
import ValidationSample from './ValidationSample';

class App extends Component {
  render() {
    return <ValidationSample />
  }
}

export default App;

이 코드의 결과는 아래 사진과 같다.

input에서는 onChange 이벤트가 발생하면 handleChange를 호출하여 state의 password 값을 업데이트하게 했다.
button에서는 onClick 이벤트가 발생하면 handleButtonClick을 호출하여 clicked 값을 참으로 설정했고, validated 값을 검증 결과로 설정했다.

input의 className 값은 버튼을 누르기 전에는 비어있는 문자열을 전달하고, 버튼을 누른 후에는 검증 결과에 따라 success 값 또는 failure 값을 설정한다. 이 값에 따라 input 색상이 초록색 또는 빨간색으로 나타난다.

DOM을 꼭 사용해야 하는 상황

앞 예제에서는 state를 사용하여 기능을 구현했지만, 가끔 state만으로 해결할 수 없는 기능이 있다.

  • 특정 input에 포커스 주기
  • 스크롤 박스 조작하기
  • Canvas 요소에 그림 그리기 등

이 때는 어쩔 수 없이 DOM에 직접적으로 접근해야 하는데, 이를 위해 바로 ref를 사용한다.

ref 사용

1. 콜백 함수를 통한 ref 설정

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

콜백 함수 사용 예시

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

이렇게 하면 앞으로 this.input은 input 요소의 DOM을 가리킨다. ref의 이름을 원하는 것으로 자유롭게 지정할 수 있다. DOM 타입과 관계없이 this.superman = ref 처럼 마음대로 지정한다.

2. createRef를 통한 ref 설정

ref를 만드는 또 다른 방법은 리액트에 내장되어 있는 createRef라는 함수를 사용하는 것이다. 이 함수를 사용해서 만들면 더 적은 코드로 쉽게 사용할 수 있다. 이 기능은 리액트 v16.3부터 도입되었으며 이전 버전에서는 작동하지 않는다.

createRef 사용 예시

import React, { Component } from 'react';

class RefSample extends Component {
  input = React.createRef();
  handleFocus = () => { this.input.current.focus(); }

  render() {
    return (
      <div>
        <input ref={this.input} />
      </div>
    );
  }
}

export default RefSample;

createRef를 사용하여 ref를 만들려면 우선 컴포넌트 내부에서 멤버 변수로 React.createRef()를 담아줘야 한다. 그리고 해당 멤버 변수를 ref를 달고자 하는 요소에 ref props로 넣어주면 ref 설정이 완료된다. 설정한 뒤 나중에 ref를 설정해 준 DOM에 접근하려면 this.input.current를 조회하면 된다.
콜백 함수를 사용할 때와 다른 점은 이렇게 뒷부분에 .current를 넣어 줘야 한다는 것이다.

지금까지 콜백 함수 혹은 createRef를 사용하여 ref를 만드는 방법을 배워 보았다. 앞으로 2가지 방법 중 편한 방법을 택하면 된다.

3. 적용

위에서 ValidationSample 컴포넌트의 렌더링 결과를 다시 한번 살펴보자.

검증하기를 누른 후의 결과이다.
검증하기를 누른 후에는 input 요소의 텍스트 커서가 보이지 않는다.
이를 검증하기를 누른 후에도 input 요소의 텍스트 커서가 보이도록 해보겠다.

import React, { Component } from 'react';
import './ValidationSample.css';

class ValidationSample extends Component {
    state = { (...) // 생략 }
    handleChange = e => { (...) // 생략 }
    handleButtonClick = () => {
        this.setState({
            clicked: true,
            validated: this.state.password === '0000'
        });
        this.input.focus();
    }

    render() {
        return (
            <div>
            <input 
              ref={(ref) => this.input=ref}
              (...) // 생략
            </div>
        );
    }
}

export default ValidationSample;

코드를 실행해보면 검증하기 버튼을 누른 후에도 텍스트 커서가 보이는 것을 확인할 수 있다.

컴포넌트에 ref 달기

1. 사용법

리액트에서는 컴포넌트에도 ref를 달 수 있다. 이 방법은 주로 컴포넌트 내부에 있는 DOM을 컴포넌트 외부에서 사용할 때 쓴다. 컴포넌트에 ref를 다는 방법은 DOM에 ref를 다는 방법과 똑같다.

<MyComponent ref={(ref) => {this.myComponent=ref}} />

이렇게 하면 MyComponent 내부의 메서드 및 멤버 변수에도 접근할 수 있다.
즉, 내부의 ref에도 접근할 수 있다. (예: myComponent.handleClick, myComponent.input 등)

2. 컴포넌트 초기 설정

이번에는 스크롤 박스가 있는 컴포넌트를 하나 만들고, 스크롤바 아래로 내리는 작업을 부모 컴포넌트에서 실행해 보겠다.

import React, { Component } from 'react';

class ScrollBox extends Component {
  render() {
    const style = {
      border: '1px solid black',
      height: '300px',
      width: '300px',
      overflow: 'auto',
      position: 'relative'
    }
    const innerStyle = {
      width: '100%',
      heigth: '650px',
      background: 'linear-gradient(white, black)'
    }
    
    return (
      <div
        style={style}
  		ref={(ref) => {this.box=ref}}>
        <div style={innerStyle}
 	  </div>
    );
  }
}

export default ScrollBox;

import React, { Component } from 'react';
import ScrollBox from './ScrollBox.';

class App extends Component {
  render() {
    return <ScrollBox />
  }
}

export default App;

3. 컴포넌트에 메서드 생성

컴포넌트에 스크롤바를 맨 아래쪽으로 내리는 메서드를 만들어 보겠다.
자바스크립트로 스크롤바를 내릴 때는 DOM 노드가 가진 다음 값들을 사용한다.

  • scrollTop: 세로 스크롤바 위치(0~350)
  • scrollHeight: 스크롤이 있는 박스 안의 div 높이(650)
  • clientHeight: 스크롤이 있는 박스의 높이(300)

새발개발자 - 스크롤 개념 참조

스크롤바를 맨 아래쪽으로 내리려면 scrollHeight에서 clientHeight 높이를 빼면 된다.

import React, { Component } from 'react';

class ScrollBox extends Component {
  scrollToBottom = () => {
    const { scrollHeight, clientHeight } = this.box;
    /* 앞 코드에는 비구조화 할당 문법을 사용했다.
       다음 코드와 같은 의미이다.
       const scrollHeight = this.box.scrollHeight;
       const clientHeight = this.box.clientHeight;
    */
    this.box.scrollTop = scrollHeight - clientHeight;
  }
  
  render() {
    (...) // 생략
    return (
      <div
        style={style}
        ref={(ref) => {this.box=ref}}>
        <div style={innerStyle}
      </div>
    );
  }
}

export default ScrollBox;

scrollToBottom 메서드의 첫번째 줄에서는 ES6의 비구조화 할당 문법을 사용했다. 이렇게 만든 메서드는 부모 컴포너트인 App 컴포넌트에서 ScrollBox에 ref를 달면 사용할 수 있다.


App 컴포넌트에서 ScrollBox에 ref를 달고 버튼을 누르면, ScrollBox 컴포넌트의 scrollTopBottom 메서드를 실행하도록 코드를 작성해보자.

import React, { Component } from 'react';
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;

요약 정리

컴포넌트 내부에서 DOM에 직접 접근해야 할 때 ref를 사용한다.
먼저 ref를 사용하지 않고도 원하는 기능을 구현할 수 있는지 반드시 고려한 후에 활용해야 된다.

서로 다른 컴포넌트끼리 데이터를 교류할 때 ref를 사용한다면 이는 잘못 사용된 것이다. 앱 규모가 커지면 마치 스파케티처럼 구조가 꼬여 버려서 유지 보수가 불가능해진다. 컴포넌트끼리 데이터를 교류할 때는 언제나 데이터를 부모 ↔ 자식 흐름으로 교류해야 한다.

아직 함수형 컴포넌트에서 ref를 사용하는 것은 배우지 않았다. 함수형 컴포넌트에서는 useRef라는 Hook 함수를 사용한다.

참고문헌

김민준,「리액트를 다루는 기술 :실무에서 알아야 할 기술은 따로 있다!」, 길벗, 개정판[실은 2판] 2019 (개정판)

profile
프론트엔드 개발자입니다.

0개의 댓글