리액트를 다루다 보면 변화는 감지해야 하지만, 해당 변화가 렌더링을 발생시키면 안되는 값을 다뤄야 하는 상황들이 종종 있습니다. 불필요한 렌더링을 방지해주는 것에 도움을 주고, 성능 최적화 측면에서도 도움을 주는 useRef hook에 대해서 정리해보겠습니다.
이전 제어 컴포넌트 포스팅에서 대부분의 Form을 구현하는데 제어 컴포넌트를 활용하는 것이 좋다라고 언급했었습니다. 이에 대한 대안인 비제어 컴포넌트는 DOM 자체에서 폼 데이터가 다뤄지는데, 모든 state 업데이트에 대한 이벤트 핸들러를 활용하는 대신, ref를 사용하여 DOM에 접근하고 해당 DOM의 값을 가져올 수 있습니다.
참고
1. 상태를 엘리먼트를 가지고 있는 컴포넌트가 관리한다면, Controlled Component
2. 엘리먼트의 상태를 관리하지 않고 엘리먼트의 참조만 컴포넌트가 소유한다면, Uncontrolled Component
일반적으로 ref를 사용하는 경우를 다음과 같이 나열할 수 있습니다.
- 텍스트 선택 영역 포커스 혹은 미디어의 재생을 관리할 때
- 애니메이션을 직접적으로 실행시킬 때
- 서드 파티 DOM 라이브러리를 React와 같이 사용할 때
예를 들면, 결제 페이지를 만든다고 가정했을 때
휴대전화 번호 또는 주소와 같은 정보는 사용자로부터 필수로 받아야 하는 데이터 입니다. 사용자가 필수 정보를 입력할 수 있도록, 해당 페이지가 렌더링되면 바로 포커싱 시켜주거나, 필수 정보를 입력하지 않고, '결제하기' 버튼을 누를 경우 해당 주소와 번호를 꼭 입력하도록 포커싱 시킬 수 있습니다.
자바스크립트에서 focus
메서드가 있습니다.
이제 예시를 들면서, useRef 훅을 어떻게 다루는 지 알아보겠습니다.
다음은 ref와 일반 변수를 설정하고, ref가 리렌더링 시에도 값이 변하지 않는지 확인해보겠습니다.
import React, { useRef, useState } from 'react'
function Ref() {
const [render, setRender] = useState(0)
const countRef = useRef(0)
let countVar = 0
const increaseRef = () => {
countRef.current += 1
}
const increaseVar = () => {
countVar += 1
}
const doRender = () => {
setRender(render + 1)
}
return (
<div>
<p>Ref : {countRef.current}</p>
<p>Var: {countVar}</p>
<button onClick={doRender}>렌더</button>
<button onClick={increaseRef}>Ref 올리기</button>
<button onClick={increaseVar}>Var 올리기</button>
</div>
)
}
export default Ref
위의 결과는 'Ref 올리기' 버튼 세 번 클릭, 'Var 올리기' 버튼 세 번 클릭 후, '렌더' 버튼을 눌러 화면을 리렌더링 시키면 발생하는 결과 입니다.
왜 countVar라는 값이 0으로 화면에 렌더링 되었을까요?
잘 생각해보면, 위의 함수형 컴포넌트도 결국 함수입니다.
렌더링이 된다는 것은 컴포넌트를 나타내는 함수가 불리는 것이며, 함수가 호출되면 함수의 내부에 있는 변수들이 초기화 됩니다.
일반적으로 우리가 let, var와 같은 키워드를 통해 선언한 변수들의 값은 초기화 됩니다.
하지만, ref는 다릅니다. 리렌더링이 발생해도, 기존의 값을 유지합니다. ref는 우리가 useEffect를 다룰 때 이야기하는 LifeCycle(생애주기), 즉 컴포넌트가 mount되고, update되고, unmount될 때까지의 전 생애주기를 통틀어 유지합니다.
참고로, ref는 {current: value}의 형태의 객체입니다.
이번의 예시는 아래와 같이 로그인 페이지를 묘사한 input 요소와 버튼이 담겨있는 컴포넌트입니다.
해당 컴포넌트 첫 렌더링 시 input 요소에 자동으로 focus 되도록 하고, 로그인 버튼을 누르면 input 요소에 작성된 value가 초기화 되고, 그 input에 focus 되도록 하는 컴포넌트로 구성하였습니다.
코드를 살펴보겠습니다.
import React, { useEffect, useRef, useState } from 'react'
function Ref() {
const inputRef = useRef()
useEffect(() => {
console.log(inputRef)
inputRef.current.focus()
}, [])
const login = () => {
alert(`환영합니다. ${inputRef.current.value}`)
inputRef.current.value = ''
inputRef.current.focus()
}
return (
<div>
<h1> Login Page <h1>
<input ref={inputRef} type="text" placeholder="username" />
<button onClick={login}>로그인</button>
</div>
)
}
export default Ref
예시 1번에서 보았듯, ref는 {current: value}
형태의 객체인데, 이 값에 접근하려면 ref.current로 접근하고, 만약 input의 value에 접근하고 싶다면 input에 ref={ref}
로 담으면 ref 객체에는 input DOM Node가 담깁니다.
원래 ref는 props가 아니기 때문에, props를 이용해서 하위 컴포넌트로 내려줄 수 없는 특징을 갖고 있습니다. 하지만, 부득이한 경우, 컴포넌트 재사용성을 위해 분리해서 작성하여, 해당 ref를 하위 컴포넌트로 내려줘야 하는 경우 forwardRef
를 다음과 같이 사용할 수 있습니다.
// Ref.js
import React, { useRef} from 'react'
import MyInput from './MyInput'
function Ref() {
const inputRef = useRef()
const focus = () => {
inputRef.current.value = ''
inputRef.current.focus()
}
return (
<div>
<h1>Login Page</h1>
<MyInput ref={inputRef} />
<button onClick={focus}>로그인</button>
</div>
)
}
export default Ref
// MyInput.js
import React, { forwardRef } from 'react'
const MyInput = (props, ref) => {
return (
<>
<input ref={ref} type="text" placeholder="아이디를 입력하세요" />
</>
)
}
export default forwardRef(MyInput)
위와 같이 export default 부분에 forwardRef로 MyInput을 감싸서, 해당 ref를 부모 컴포넌트로부터 받을 수 있습니다. 또는 아래와 같이 사용가능합니다.
const MyInput = forwardRef((props, ref) => {
return (
<>
<input ref={ref} type="text" placeholder="아이디를 입력하세요" />
</>
)
})
export default MyInput
화살표 함수 전체를 forwardRef로 감싸주면 됩니다.
이렇게 비제어 컴포넌트를 ref를 이용해서 만들고, DOM에 대해 접근하는 법에 대해서 정리해 보았습니다.
다시 한 번 정리하자면, 로그인 input의 경우 유효성 검사 로직이 state가 변함에 따라 실행되어야 하므로 제어 컴포넌트로 활용할 수 있습니다. 제어 컴포넌트에서 form 데이터는 React 컴포넌트에서 다뤄집니다.
하지만, 직접적으로 form의 DOM 요소를 참조하고 제어를 하는 경우 useRef를 이용해 비제어 컴포넌트로 활용할 수 있습니다.
form 요소 컨트롤에 대해서만 다뤘지만, 이후에 프로젝트들을 다뤄 보면서 애니메이션이나 비디오의 재생과 관련하여 useRef를 사용하게 되면, 정리해보는 시간이 있으면 좋겠습니다.