React를 사용해 개발을 하다보면, 필연적으로 React Hooks를 마주하게 된다. useState, useEffect부터 시작해서 useRef, useMemo 등등... 그 중에서도 useRef가 특히 완전히 이해가 잘 안 됐었기에 useRef부터 정리해보려 한다.
우선 React Hooks라는 것에 대해 간단히 짚고 넘어가기 위해 React 공식 홈페이지에 있는 설명을 가져왔다.
Hook은 React 버전 16.8부터 React 요소로 새로 추가되었습니다. Hook을 이용하여 기존 Class 바탕의 코드를 작성할 필요 없이 상태 값과 여러 React의 기능을 사용할 수 있습니다.
클래스형 컴포넌트에 어떤 문제가 있었길래 react hooks가 도입되었을까?
복잡성: 클래스형 컴포넌트는 복잡성이 높아서 코드가 길어지고 가독성이 떨어질 수 있다.
재사용성: 클래스형 컴포넌트에서는 상태 관리를 위해 공통 로직을 가진 라이프사이클 메소드를 작성하기가 어려워서, 코드 재사용성이 떨어질 수 있다.
유지보수성: 클래스형 컴포넌트에서는 라이프사이클 메소드를 여러 개 작성하다 보면, 어떤 메소드가 어떤 용도로 사용되는지 파악하기 어렵다. 또한, 클래스형 컴포넌트에서는 this 바인딩을 주의해야 하므로, 코드를 작성하기가 더 복잡하다.
나처럼 react를 접한지 얼마 안 되었다면 클래스형 컴포넌트를 사용할 일이 거의 없기 때문에 크게 와닿지 않을 수 있다. 여러 문제들 중 비교적 간단히 살펴볼 수 있는 this 바인딩을 살펴보자.
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
handleClick() {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick}>Increase Count</button>
</div>
);
}
}
위 코드에서 handleClick() 메소드 내부에서 this 키워드를 사용하면, this는 handleClick() 함수 자체를 가리킨다. 따라서 handleClick() 함수 내부에서 this.setState()를 호출하면, this가 가리키는 값이 undefined가 되어 오류가 발생한다.
Reack Hooks와 함수형 컴포넌트로 개선하면 흔히 보던 간단한 코드가 완성된다.
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increase Count</button>
</div>
);
}
useRef
는 저장공간 또는 DOM요소에 접근하기 위해 사용되는 React Hook이다.useRef
를 사용하면 함수형 컴포넌트에서도 클래스형 컴포넌트에서처럼 DOM 요소를 직접 조작하거나 값을 저장할 수 있다.
useRef는 다음과 같은 경우에 사용된다.
React는 virtual DOM을 사용해, DOM 조작을 최소화 하고 하지만 일부 경우(외부 라이브러리와 통합, 특정 요소에 포커스 등)에서는 DOM을 조작해야할 수 있다.
useRef를 사용한 예시 코드이다,
import React, { useRef } from "react";
function TextInput() {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus();
};
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={handleClick}>Focus input</button>
</div>
);
}
버튼을 클릭하면, input 영역에 focus 할 수 있게된다.
※ cf. querySelector VS useRef
DOM요소에 접근한다고 하니 querySelector
가 생각이 났다.
querySelector
는 DOM에서 특정 요소를 선택하는 메서드로, 이 메서드를 사용하여 선택한 요소의 속성을 변경하거나 이벤트를 추가할 수 있다.
useRef
는 React 컴포넌트에서 사용되며, 이전 값을 유지하는 데에도 사용될 수 있다는 차이점이 있다.
코드를 작성하다보면, 상태의 변화 뿐 아니라 초기 설정한 값도 필요한 경우가 있다.
import { useRef, useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
// 이전 count 값을 prevCountRef.current에 저장
prevCountRef.current = count;
const handleClick = () => {
setCount(count + 1);
console.log("현재 count:", count); // 1
console.log("이전 count:", prevCountRef.current); // 0
};
return (
<div>
<p>현재 카운트: {count}</p>
<button onClick={handleClick}>카운트 증가</button>
</div>
);
}
prevCountRef.current
에 이전의 count 값을 저장해놓고 있으므로, count 값이 변경되더라도 prevCountRef.current는 이전 상태를 유지한다.
참고로, useRef는 내용이 변경될 때 그것을 알려주지는 않는다!
따라서 내용이 변경될 때마다 다시 렌더링이 발생하지 않으며, ref를 통해 DOM 요소를 조작할 때는 실제 DOM 요소와의 일관성을 유지해야 함에 유의하자.
예를 들어,
import React, { useRef } from "react";
function TextInput() {
const inputRef = useRef(null);
const handleChange = () => {
inputRef.current.value = inputRef.current.value.toUpperCase();
};
return (
<div>
<input type="text" ref={inputRef} onChange={handleChange} />
</div>
);
}
위의 코드에서는 current 프로퍼티를 사용해 실제 DOM 요소의 값을 함께 변경하므로 사용자가 입력한 값과 state 값이 항상 일치한다.
이처럼 useRef를 통해 값을 변경하면 불필요한 렌더링을 줄일 수 있겠지만, react가 변경된 값에 대해 알지 못하므로 useEffect와 함께 활용하거나 혹은 useState를 활용해 변경할 수도 있다.