티스토리 뷰

WEB/React

[React] ref: DOM에 이름 달기

춘햄 2023. 3. 24. 14:12

일반 HTML에서 DOM 요소에 이름을 달 떄는 보통 id를 사용한다. 특정 DOM 요소에 id를 달아서 CSS 스타일을 적용하거나 자바스크립트에서 해당 id를 가진 요소를 찾아서 작업할 수가 있다. 

 

기본 리액트 프로젝트도 처음 생성을 하면 div 요소에 root라는 아이디를 달아서 

const root = ReactDOM.createRoot(document.getElementById('root'));

와 같이 사용한다.

 

HTML 에서 id를 사용하여 DOM에 이름을 다는 것처럼 리액트 프로젝트 내부에서 DOM에 이름을 다는 방법이 있다. 

 

바로 ref(reference)이다.

 

리액트 컴포넌트 안에서도 id를 사용활 수 있지만, JSX 안에서 DOM에 id를 달면 해당 DOM을 렌더링할 때 그대로 전달된다. 이러면 같은 컴포넌트를 여러 번 사용하게 되면 DOM의 id가 유일하게 되지 않으므로 문제가 생기기 때문에, 특수한 경우가 아니라면 id 사용을 권장하지는 않는다.

 

바로 알아보도록 하자.


ref는 어떤 상황에?

 ref는 리액트에서 DOM을 반드시 직접적으로 건드려야 할 때 사용한다. 

 

일반 자바스크립트로 어떤 사이트를 만들 때, input 태그에 아이디를 달아 아래와 같은 코드로 검증하려고 하면,

let input = document.getElementById('password');

와 같이 반드시 DOM에 접근하여 값을 얻어야 한다.

 

하지만 리액트에서 이런 작업은 굳이 DOM에 접근하지 않아도 state로 구현할 수 있다. 

 

즉, 아래와 같이 작성할 수 있다는 이야기이다.

 

◎ValidationSample.css

.success {
    background-color: lightgreen;
}

.failure {
    background-color: lightcoral;
}

 

◎ValidationSample.js

import {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="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

비밀번호가 0000일 때는 배경색을 초록으로, 틀렸을 때는 빨강색으로 바꾸는 정도의 예제는 위와 같이 state만을 사용하여 충분히 나타낼 수 있다.

 

그러나 특정 input에 포커스를 주거나, 스크롤 박스를 조작하고 Canvas에 그림을 그리는 등의 작업은 어쩔 수 없이 DOM에 직접적으로 접근하여야 하는데, 이를 위해 ref를 사용하는 것이다.


ref 사용

 이제 프로젝트에서 ref를 사용해보자. ref를 사용하는 방법은 두 가지이다.

 

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

ref를 만드는 가장 기본적인 방법은 콜백 함수를 사용하는 것이다. ref를 달고자 하는 요소에 ref라는 콜백 함수를 props로 전달해주면 된다. 

 

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

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

이렇게 작성해주면, 앞으로 this.input은 input 요소의 DOM을 가리킨다. ref의 이름은 원하는 것으로 자유롭게 지정할 수 있다. 

 

위에는 input으로 지정했지만, DOM 타입과 관계없이 this.superman = ref 처럼 마음대로 지정해도 된다.

 

2. createRef를 통한 ref 설정

 ref를 만드는 또 다른 방법은 리액트에 내장되어 있는 createRef 라는 함수를 사용하는 것이다. 이 함수를 사용하여 만들면 1번 보다는 더 적은 코드로 쉽게 사용할 수 있다. 

 

단, 이 기능은 리액트 v16.3 부터 도입되었으며, 이전 버전에는 작동하지 않는다.

import {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를 조회하면 된다. 


그럼 위 비밀번호 예제 코드를 수정해보자.

 

우선, input 태그에 아래와 같이 ref 콜백 함수를 추가한 뒤에,

 <input
    ref={(ref)=>this.input=ref}
    type="password"
    value={this.state.password}
    onChange={this.handleChange}
    className={this.state.clicked ? (this.state.validated ? 'success' : 'failure') : ''}
/>

ValidationButton 클릭 이벤트를 수정하여 클릭하면 포커스가 가도록 설정해주면 된다.

handleButtonClick = () => {
    this.setState({
        clicked: true,
        validated: this.state.password === '0000'
    });

    this.input.focus();
}

컴포넌트에 ref 달기

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

 

그 방법 자체는 동일하다.

 

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

이렇게 하면 MyComponent 내부의 메서드 및 멤버 변수에도 접근할 수 있다. 즉, 내부의 ref에도 접근할 수 있다는 것이다. 

 

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

 

◎ScrollBox.js

import {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%',
            height: '650px',
            background: 'linear-gradient(white, black)'
        };
        
        return (
            <div
                style={style}
                ref={(ref) => {this.box = ref}}>
                <div style ={innerStyle}/>
            </div>
        )
    }
}

export default ScrollBox;

 

◎App.js

import ScrollBox from "./ScrollBox";

const App = () => {
     return <ScrollBox ref={(ref) => this.scrollBox=ref}/>
};

export default App;


이제 컴포넌트에 스크롤바를 맨 아래쪽으로 내리는 메서드를 만들자. 

 

◎ScrollBox.js

scrollToBottom = () => {
    const {scrollHeight, clientHeight} = this.box;

    this.box.scrollTop = scrollHeight - clientHeight;
}

 

ref를 이용하면 아래와 같이 App.js 에서 위 메서드를 핸들링할 수 있다. 단, 이때 App.js 가 this를 받을 수 있도록 클래스형 컴포넌트로 바꿔줘야 한다.

 

◎App.js

import ScrollBox from "./ScrollBox";
import {Component} from "react";

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

export default App;


컴포넌트와 컴포넌트를 ref를 사용하여 서로 제어할 수 있지만, 서로 다른 컴포넌트끼리 데이터를 교류할 때 ref를 사용한다면, 이는 잘못된 설계이다. 컴포넌트끼리 데이터를 교류할 때는 언제나 데이터를 부모<=>자식 흐름으로 교류해야 한다. 

 

컴포넌트 내부에서 반드시 DOM에 직접 접근해야 할 때만 ref를 사용한다는 점을 주의하자.

 

아직 함수 컴포넌트에서 ref를 사용하는 것은 다루지 않았는데, Hooks의 useRef를 사용하여 동일하게 사용할 수 있다.

 

이는 나중에 Hooks를 다루며 자세히 알아보려고 한다.

 

'WEB > React' 카테고리의 다른 글

[React] 컴포넌트의 라이프사이클  (0) 2023.03.24
[React] 컴포넌트 반복  (0) 2023.03.24
[React] 이벤트 핸들링  (0) 2023.03.23
[React] Component  (0) 2023.03.23
[React] JSX 문법  (0) 2023.03.23
Comments