요즘 커스텀 훅을 만들어보고 해당 부분을 다시 공부하면서 내가 자주사용하고, 또는 자주 사용해보지 않았던 훅들을 정리해보려고 한다.
1. 리액트 훅이란?
리액트 훅은 리액트 16.8 버전에 새롭게 도입된 기능으로, 함수형 컴포넌트에서 상태(state)와 라이프사이클 기능을 사용할 수 있게 해준다. 기존의 클래스형 컴포넌트보다 훨씬 간결하고 읽기 쉬운 코드를 작성할 수 있도록 도와주며, 코드 재사용성을 높여준다는 장점이 있다. 리액트 훅을 사용하면 클래스 없이도 상태를 관리하고, 컴포넌트의 생명주기에 따라 작업을 수행할 수 있는데, 이를 통해 코드의 가독성이 향상되며 함수형 프로그래밍의 장점을 더욱 효과적으로 활용할 수 있게 된다.
2. 리액트 훅의 규칙
리액트 훅을 사용할 때는 몇 가지 중요한 규칙을 따라하는데, 해당 규칙을 따르지 않으면 예기치 않은 버그를 만날 수 있다.
1) 훅은 최상위에서만 호출해야한다.
훅은 컴포넌트의 최상위 레벨에서 호출되어야한다. 리액트가 훅 호출 순서를 추적하여 상태를 유지하기 위해 조건문, 반복문, 중첩된 함수 안에서 훅을 호출해서는 안된다. 리액트 훅은 호출되는 순서에 의존하기 때문에 조건문이나 반복문 안에서 실행하게 될 경우 해당 부분을 건너뛰는 일이 발생할 수도 있기 때문에 순서가 꼬여 버그가 발생할 수 있기 때문이다.
2) 훅은 리액트 함수 컴포넌트 또는 커스텀 훅에서만 호출해야한다.
훅은 일반 자바스크립트 함수에서 호출할 수 없고, 반드시 리액트 함수 컴포넌트 또는 커스텀 훅 내부에서만 호출해야한다. 이는 훅의 동작을 보장하고, 리액트의 상태관리 체계를 유지시킨다.
3. 자주 사용하는 리액트 훅
리액트는 여러가지 내장 훅을 제공하는데 그 중에서도 자주 사용되는 훅은 다음과 같다.
1) useState : 컴포넌트의 상태를 관리하는 훅
컴포넌트 내에서 상태를 관리할 수 있게 해주는 훅으로 아래와 같이 사용한다. 먼저 count라는 변수의 초기값은 0으로 설정되어있다. 이 때, button을 클릭할 경우 setCount 함수를 통해 count에 1을 더하여 새로운 값으로 변경시켜준다.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
2) useEffect : 컴포넌트가 렌더링될 때마다 특정 작업을 수행할 수 있게 해주는 훅
해당 훅은 데이터 패칭, 구독 설정, 수동으로 DOM을 업데이트할 경우 유용하게 사용되며 생명주기 메서드인 componentDidMount, componentDidUpdate, componentWillUnmount를 대체할 수 있다.
import React, { useState, useEffect } from 'react';
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(timer); // cleanup function
}, [count]);
return <div>{count}</div>;
}
3) useContext : 리액트의 Context API를 쉽게 사용할 수 있게 해주는 훅
해당 훅은 리액트의 컨텍스트 API와 함께 사용되어 컴포넌트 트리 전체에서 전역적인 데이터를 쉽게 공유할 수 있게 해준다.
import React, { useContext } from 'react';
const MyContext = React.createContext();
function MyComponent() {
const value = useContext(MyContext);
return <div>{value}</div>;
}
4) useReducer : 복잡한 상태 로직을 관리할 때 사용하는 훅
useState의 대안으로 상태와 상태를 변경하는 로직을 분리할 수 있다는 특징이 있다.
import React, { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}
5) useMemo : 성능 최적화에 유용한 훅으로 메모이제이션된 값을 반환
해당 훅은 연산 비용이 큰 계산을 캐싱하여 컴포넌트가 다시 렌더링될 때 불필요한 계산을 피할 수 있도록 도와준다. 첫번째 인자로 계산할 함수를, 두번째 인자로 의존성 배열을 받게 되는데 의존성 배열의 값이 변경되지 않는 한, 이전에 계산된 값을 반환한다는 특징이 있다.
import React, { useMemo } from 'react';
function ExpensiveComponent({ num }) {
const computedValue = useMemo(() => {
// 연산 비용이 큰 계산
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += num;
}
return result;
}, [num]); // num이 변경될 때만 재계산
return <div>Computed Value: {computedValue}</div>;
}
6) useCallback : 성능 최적화에 유용한 훅으로 메모이제이션된 콜백 함수를 반환
해당 훅은 함수가 재정의 되는 것을 방지하여, 하위 컴포넌트에 전달되는 콜백함수가 불필요하게 변경되는 것을 막아준다. 첫번째 인자로 생성할 함수를, 두번째 인자로 의존성 배열을 받게 되는데 의존성 배열의 값이 변경되지 않는 한, 이전에 생성된 함수를 반환한다는 특징이 있다.
import React, { useState, useCallback } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []); // 의존성 배열이 비어 있으므로 처음 렌더링 시에만 함수 생성
return (
<div>
<ChildComponent increment={increment} />
<p>Count: {count}</p>
</div>
);
}
function ChildComponent({ increment }) {
console.log('ChildComponent 렌더링');
return <button onClick={increment}>Increment</button>;
}
7) useRef : 변하지 않는 값을 유지하거나 DOM 요소에 직접 접근할 수 있게 해주는 훅
해당 훅은 컴포넌트의 렌더링 사이에서 변하지 않는 값을 유지하거나 DOM 요소에 직접 접근할 수 있게 도와준다. useRef를 사용한 값의 변경은 컴포넌트를 다시 렌더링시키지 않는다.
import React, { useRef } from 'react';
function Timer() {
const countRef = useRef(0);
const handleClick = () => {
countRef.current += 1;
console.log(`Count: ${countRef.current}`);
};
return (
<div>
<button onClick={handleClick}>Increment</button>
</div>
);
}
위의 예제에서 conutRef는 렌더링 사이에 유지되지만, 값이 변경되어도 컴포넌트가 다시 렌더링되지는 않는다. 그렇기 때문에 이는 상태를 관리할 필요가 없고, 값이 변경될 때마다 UI를 업데이트할 필요가 없는 경우에 유용하게 사용할 수 있다.
또한 값변경이 아닌 DOM에 접근하기 위한 방식으로 해당 훅을 사용할 수도 있는데 예제는 다음과 같다.
import React, { useRef, useEffect } from 'react';
function FocusInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
inputRef는 useRef를 통해 생성된 레퍼런스를 가지고 있으며, 이 레퍼런스를 input 요소의 ref 속성에 전달한다. 위에서 이야기했던 useEffect 훅을 사용하여 컴포넌트가 처음 렌더링된 후, input 요소에 포커싱을 설정하는 예제이다.