React Hooks 프로젝트에 도입하기 - useState와 useEffect 위주로
배경
2월 초 React 16.8이 릴리즈되면서 Hooks가 정식 기능으로 추가되었다. 작년 말부터 관심은 있었지만 프로덕션에 적용하기엔 이르다고 판단했고, 이제는 새로운 컴포넌트부터 Hooks로 작성하기로 결정했다.
기존 코드의 문제점
우리 프로젝트는 클래스 컴포넌트와 HOC 패턴이 주를 이뤘다. 특히 인증, 로딩 상태 같은 공통 로직을 HOC로 처리하다 보니 wrapper hell이 심각했다.
export default withAuth(withLoading(withRouter(UserProfile)));
또한 lifecycle 메서드에서 관련 없는 로직들이 섞여 있어 유지보수가 어려웠다.
useState로 상태 관리 단순화
가장 먼저 적용한 건 간단한 폼 컴포넌트였다.
function SearchForm() {
const [query, setQuery] = useState('');
const [filters, setFilters] = useState({ category: 'all' });
const handleSubmit = (e) => {
e.preventDefault();
onSearch(query, filters);
};
return (
<form onSubmit={handleSubmit}>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
{/* ... */}
</form>
);
}
클래스 컴포넌트 대비 boilerplate가 확실히 줄어들었다. this 바인딩 걱정도 없어졌다.
useEffect로 side effect 처리
API 호출이나 구독 로직은 useEffect로 처리했다.
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetchUser(userId)
.then(data => setUser(data))
.finally(() => setLoading(false));
}, [userId]);
if (loading) return <Spinner />;
return <div>{user.name}</div>;
}
dependency array를 제대로 관리하는 게 핵심이다. eslint-plugin-react-hooks를 설정해서 빠뜨린 dependency를 경고받도록 했다.
커스텀 Hook으로 로직 재사용
HOC를 대체할 수 있는 가장 큰 장점이다.
function useAuth() {
const [user, setUser] = useState(null);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(setUser);
return unsubscribe;
}, []);
return user;
}
function Dashboard() {
const user = useAuth();
if (!user) return <Redirect to="/login" />;
return <div>Welcome {user.name}</div>;
}
주의할 점
- 조건부로 Hook을 호출하면 안 된다 (React의 Hook 규칙)
- useEffect의 cleanup 함수를 잊지 말 것
- dependency array를 정확히 관리해야 무한 루프를 방지할 수 있다
소감
Hooks 도입 후 코드가 확실히 간결해졌고, 로직 재사용도 직관적이다. 기존 클래스 컴포넌트를 급하게 리팩토링할 필요는 없지만, 새 코드는 Hooks로 작성하는 게 합리적이라고 판단했다.