** 이 글은 react 16.2 버전 이후 기준으로 작성되었습니다. **
리액트에서는 직접적으로 DOM 요소에 접근하는 것을 권장하지 않는다.
부득이하게 DOM 요소에 접근해야 할 때에는 ref 속성을 이용하여 특정 DOM 요소에 접근할 수 있다.
클래스형 컴포넌트에서는 createRef, 함수 컴포넌트에서는 useRef를 사용하여 ref 변수를 생성할 수 있다.
함수 컴포넌트에서도 createRef를 사용할 수는 있지만, 컴포넌트가 리렌더링될 때마다 ref 값이 null로 초기화되어 화면에 표출되는 원하는 값을 얻을 수 없으므로 useRef를 사용하는 것을 권장한다.
import React, { useState, createRef } from 'react';
const App = () => {
const ref = createRef(0);
const [_, setState] = useState(false);
const handleClick = () => {
ref.current = 123;
setState(true);
console.log(ref.current); // 123
};
return (
<>
<span>{ref.current}</span> {/* 123이 표출되지 않음 */}
<button onClick={handleClick}>증가</button>
</>
);
};
export default App;
ref 속성은 다음과 같은 상황에 주로 사용된다.
- input 요소의 속성(value, focus 등)에 접근해야 할 때
- DOM 요소의 애니메이션을 직접적으로 실행해야 할 때
- 서드 파티 DOM 라이브러리를 사용할 때
사용 방법은 다음과 같다.
ref가 가리키는 DOM 요소에 접근할 때는 ref의 current 속성으로 접근할 수 있다.
- 클래스형 컴포넌트
import { Component, createRef } from 'react';
class index extends Component {
constructor() {
super();
this.ref = createRef();
}
handleClick() {
console.log(this.ref.current.value);
}
render() {
return (
<main>
<input type="text" ref={this.ref} />
<button type="button" onClick={this.handleClick.bind(this)}>
Enter Text
</button>
</main>
);
}
}
export default index;
- 함수 컴포넌트
import { useRef } from 'react';
const Main = () => {
const ref = useRef();
const handleClick = () => {
console.log(ref.current.value);
};
return (
<main>
<input type="text" ref={ref} />
<button type="button" onClick={handleClick}>
Enter Text
</button>
</main>
);
};
export default Main;
그럼 위 예시처럼 일반적인 HTML 태그가 아닌 커스텀 컴포넌트에 ref를 적용하고자 할 때는 어떻게 하면 될까?
간단하게 부모 컴포넌트에서 useRef 값을 props로 전달하여 해당 자식 컴포넌트의 ref 속성에 넘겨주는 방식을 생각할 수 있다.
하지만 실제로 이런 방식은 클래스형 컴포넌트에서는 문제가 없지만, 함수 컴포넌트로 구현하게 되면 ref.current에 접근했을 때 다음과 같은 오류를 겪게 된다.
함수 컴포넌트는 클래스형 컴포넌트와는 달리 인스턴스가 없기 때문에 ref 속성을 사용할 수 없기 때문이다.
함수 컴포넌트에서 ref를 커스텀 컴포넌트에 전달하여 사용하고자 할 때는 forwardRef라는 함수를 사용해야 한다.
사용법은 간단하다.
1. ref를 지정하고자 하는 커스텀 컴포넌트에 ref 속성을 추가한다.
const emailRef = useRef();
<Input
id="email"
ref={emailRef}
label="E-Mail"
value={enteredEmail}
isValid={emailIsValid}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
2. 커스텀 컴포넌트의 2번째 파라미터로 ref를 추가한다. (props, ref) 형태로 수정하면 된다.
3. 커스텀 컴포넌트 내 실제로 ref를 지정하고자 하는 요소에 ref 속성을 추가한다.
4. 해당 커스텀 컴포넌트를 export할 때, forwardRef 함수로 감싼다.
import { forwardRef } from 'react';
import classes from './Input.module.css';
const Input = ({ label, id, value, isValid, onChange, onBlur }, ref) => {
return (
<div className={`${classes.control} ${isValid ? '' : classes.invalid}`}>
<label htmlFor={id}>{label}</label>
<input type={id} ref={ref} id={id} value={value} onChange={onChange} onBlur={onBlur} />
</div>
);
};
export default forwardRef(Input);
forwardRef는 보통 useImperativeHandle과 함께 사용된다.
useImperativeHandle은 forwardRef를 사용하는 하위 컴포넌트에서 선언한 변수 혹은 함수를 상위 컴포넌트에서 실행할 수 있도록 하는 hook이다.
간단히 말하자면, 부모 컴포넌트에서 자식 컴포넌트의 값에 접근하거나 함수를 실행할 수 있다.
사용 방법은 다음과 같다.
첫 번째 인자로 전달 받은 ref, 두 번째 인자로 key: value 형태의 객체를 반환하는 함수를 전달하면 된다.
두 번째 인자에서 접근하는 ref는 useRef를 이용하여 별도의 ref를 생성하여 사용하면 된다.
이 때, 기존 하위 컴포넌트에서 사용하고 있던 ref 속성의 값도 새로 생성한 ref 변수로 변경해야 한다.
const inputRef = useRef();
useImperativeHandle(ref, () => ({
setFocus: () => {
inputRef.current.focus();
},
}));
return (
<div className={`${classes.control} ${isValid ? '' : classes.invalid}`}>
<label htmlFor={id}>{label}</label>
<input type={id} ref={inputRef} id={id} value={value} onChange={onChange} onBlur={onBlur} />
</div>
);
forwardRef와 useImperativeHandle을 모두 적용한 전체 코드는 다음과 같다.
import { useRef, forwardRef, useImperativeHandle } from 'react';
import classes from './Input.module.css';
const Input = ({ label, id, value, isValid, onChange, onBlur }, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
setFocus: () => {
inputRef.current.focus();
},
}));
return (
<div className={`${classes.control} ${isValid ? '' : classes.invalid}`}>
<label htmlFor={id}>{label}</label>
<input type={id} ref={inputRef} id={id} value={value} onChange={onChange} onBlur={onBlur} />
</div>
);
};
export default forwardRef(Input);
참고로, 리액트에서는 되도록 ref를 사용하기 보다는 선언적으로 프로그래밍하는 것을 권장하고 있다.
반드시 필요한 경우에만 사용하도록 하자.
'Frontend > React' 카테고리의 다른 글
react-router-dom 버전 5 vs 6 비교 및 차이점 알아보기 (2) | 2022.04.19 |
---|---|
Redux Toolkit - A non-serializable value was detected in an action, in the path: `type` 오류 해결 (2) | 2022.04.01 |
React - 2. 리액트 컴포넌트 스타일링하기 (2) | 2022.01.16 |
React - 1. 리액트 입문 정리 (2) | 2021.11.01 |
React.js - JSX에서 문자열(String) 타입을 HTML으로 렌더링 (0) | 2021.06.17 |