
프론트 엔드 개발자 공부를 하면서 현재까지 제일 많이 사용하고있지만, 정리는 안했던 그 리액트 훅에 대해 정리해보려고 한다.
React 는 Facebook에서 발표된 사용자의 인터페이스에 집중한 자바스크립트 라이브러리이다.
React가 나오기 전에 이미 자바스크립트 기반의 많은 라이브러리와 프레임 워크는 존재했다. 대부분이 MVC, MVVM, MVW 패턴을 기반으로 만들어졌고, 이 패턴들의 공통점은 뷰와 양방향으로 바인드되어, 뷰가 변경되면 모델도 업데이트된다는점이다.
🧐 양방향 바인딩이란?
뷰가 변경되면 모델도 자동으로 업데이트되고, 모델이 변경되면 뷰도 자동으로 변경되는 구조
해당 모델들은 데이터의 상태 변경이 즉각적으로 일어나게 해주는 mutaion을 권장한다.
🔹 예시: Vue.js에서 v-model을 사용할 때
<template>
<input v-model="count" />
<button @click="count++">증가</button>
<p>현재 값: {{ count }}</p>
</template>
<script>
export default {
data() {
return {
count: 0
};
}
};
</script>
그러나, DOM 변화의 발생 = 렌더링 이라는뜻이고 이는 곧 잦은 DOM의 변화는 많은 비용이 든다는 소리로도 해석된다.
여기에 초점을 둔게 바로 React

React는 생명 주기(라이프 사이클) 를 가지고, 각각의 사이클에서 이벤트를 갖는다.
이러한 라이프 사이클 이벤트를 기반으로 컴포넌트의 동작을 제어하고, 컴포넌트의 작업 수행을 향상시키는 사용자 정의 로직을 구현한다.
리액트의 생명주기 : 컴포넌트가 생성되고 사용되고 소멸될 때 까지 일련의 과정
1️⃣ 마운트(Mount)
: 컴포넌트가 처음 실행이 될때 생성단계
getDerivedStateFromProps
- Props로 받아온 것을 state에 설정하고 싶을 때 사용
render
- 컴포넌트를 렌더링 해주는 메서드
componentDidMount
- 컴포넌트가 마운트, 첫번째 렌더링이 된 직후 호출되는 메서드
2️⃣ 업데이트(Update)
: 컴포넌트 업데이트시 (상태나 props 변경 시)
getDerivedStateFromProps
shouldComponentUpdate
- 컴포넌트를 다시 리렌더링 할지 말지 결정하는 메서드 (성능최적화)
render
getSnapshotBeforeUpdate
- 가장 마지막으로 렌더링된 결과가 DOM 등에 반영되기 전에 호출
componentDidUpdate
- 갱신이 일어난 직후에 호출되는 메소드
3️⃣ 언마운트(Unmount)
: 컴포넌트가 화면에서 제거 되는것
위와 같은 리액트의 라이프 사이클 메소드들은 클래스형 컴포넌트에서만 사용이 가능했다. (함수형 컴포넌트에서는 UI렌더링만 하는 역할이였음)
=> 하나의 기능을 구현하는 코드가 여러 라이프사이클 메서드에 흩어져서 유지보수 및 재사용하기 어렵고, 'this' 키워드의 사용으로 동작이 어렵다는 문제점이 있었다.
이를 해결하기 위해 리액트 16.8 이후에 Hook이 등장했다.
Hook은 함수형 컴포넌트에서 상태관리 및 리액트 생명주기 기능을 사용할 수있도록 해주는 함수

컴포넌트 최상위에서만 사용할 수 있다. (반복문, 조건문, 중첩된 함수내 에서는 X)
리액트 Hook은 호출되는 순서를 기준으로 상태를 관리한다.
=> Hook은 내부적으로 배열 형태의 상태 리스트에 저장.
=> 각 useState 호출은 해당 렌더링에서 호출된 순서대로 저장된 상태를 참조.
=> 만약 Hook 호출 순서가 달라지면 React는 어떤 Hook이 어떤 상태를 참조하는지 알 수 없게 돼서 오류가 발생.
[예시]
function Component() {
const [count, setCount] = useState(0); // Hook #1
const [name, setName] = useState("John"); // Hook #2
return <div>{count} {name}</div>;
}
위에 예시 코드로 상태를 선언하면, 리액트는 아래와 같이 상태를 저장한다.
[리액트에서 내부적으로 상태를 저장하는 방식]
const hooks = [
{ state: 0 }, // 첫 번째 useState
{ state: "John" } // 두 번째 useState
];
근데 만약 여기서 순서가 바뀌면 아래와 같이 훅의 호출 순서가 바뀌게 된다.
[잘못된 코드]
function Component({ showName }) {
const [count, setCount] = useState(0); // Hook #1 (Index 0)
if (showName) {
const [name, setName] = useState("John"); // 🚨 Hook #2 (Index 1 or 미실행)
}
return <div>{count}</div>;
}
showName === true → useState("John")이 실행됨
showName === false → useState("John")이 실행되지 않음
즉, Hook의 호출 순서가 달라짐!
-> 두 번째 Hook(useState("John"))이 실행되지 않으면, React는 기존 Hook 배열에서 상태를 찾지 못함!
함수형 컴포넌트에서만 호출 가능
비동기함수는 콜백함수로 사용할 수 없다.
useEffect(async () => { // ❌ useEffect의 콜백이 async
const data = await fetchData();
setState(data);
}, []);
async를 붙인 함수는 항상 Promise를 반환하게되는데 리액트는 훅 콜백이 Promise를 반환할 것이라고 예상하지 않기때문에 예상치 못한 문제가 발생할 수 있다고한다.
(+ useEffect는 유일하게 함수형 컴포넌트에서 라이프 사이클 메소드를 대체할 수 있는 함수이기때문에)
대표적인 상태관리를 해주는 훅으로 초기값을 설정하고, 상태를 업데이트하는 함수이다.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // count 상태와 setCount 업데이트 함수 정의
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
</div>
);
}
유의할점
React는 상태 업데이트가 발생할 때마다 컴포넌트를 다시 렌더링 해야한다. 만약 상태 업데이트 함수가 동기적으로 실행된다면, 상태가 업데이트될 때마다 렌더링을 반복하게 되어 성능 저하가 발생할 수 있다.
비동기적으로 상태를 처리하는 이유는 여러 상태 업데이트가 동시에 일어날 때, React가 효율적으로 한 번의 렌더링으로 처리할 수 있도록 하기 위해서이다. (= Auto Batching)
위에서 언급했듯이 유일하게 클래스형 컴포넌트의 생명주기 기능 메소드를 대체할 수 있는 함수.
컴포넌트가 렌더링된 후에 부수 효과(side effects) 를 처리할 때 사용 (예: 데이터 fetching, 타이머 설정 등)
import { useState, useEffect } from 'react';
function Timer() {
const [time, setTime] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setTime(prevTime => prevTime + 1);
}, 1000);
return () => clearInterval(interval); // 컴포넌트 언마운트 시 타이머 정리
}, []); // 빈 배열: 컴포넌트가 마운트될 때만 실행
return <div>{time}초</div>;
}
유의할점
함수의 참조를 메모이제이션하여, 불필요한 렌더링을 방지하는 데 사용한다.
import React, { useState, useCallback } from 'react';
export default function App() {
const [count, setCount] = useState(0);
// 함수 메모이제이션: setCount는 count가 변경될 때만 재생성됨
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]); // count가 변경될 때만 함수가 새로 생성됨
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increase Count</button>
</div>
);
}
handleClick 함수는 count가 변경될 때만 새로 생성된다.
useCallback을 사용하면, handleClick 함수가 매 렌더링마다 새로 생성되는 것을 방지할 수 있다.
만약 count가 변경되지 않으면, 이전 handleClick 함수가 계속 사용되므로 성능 최적화가 될 수 있다.
유의할점
useCallback이 함수를 메모이제이션 했다면 useMemo는 값을 메모이제이션한다.
의존성 배열에 들어있는 값이 변경되었을때 연산을 다시한다.
import React, { useState, useMemo } from 'react';
export default function App() {
const [count, setCount] = useState(0);
const [multiplier, setMultiplier] = useState(1);
// 값 메모이제이션: count와 multiplier가 변경될 때만 계산이 다시 이루어짐
const computedValue = useMemo(() => {
console.log('Computing value...');
return count * multiplier;
}, [count, multiplier]); // count와 multiplier가 변경될 때만 재계산
return (
<div>
<p>Computed Value: {computedValue}</p>
<button onClick={() => setCount(count + 1)}>Increase Count</button>
<button onClick={() => setMultiplier(multiplier + 1)}>Increase Multiplier</button>
</div>
);
}
computedValue는 count와 multiplier가 변경될 때만 계산된다.
버튼을 클릭할 때마다 setCount나 setMultiplier를 호출해 값을 변경하지만, 값이 변경되지 않은 경우에는 console.log('Computing value...')가 출력되지 않는다.
유의할점
useReducer는 상태 관리가 복잡할 때, 특히 여러 상태를 다루거나 상태 변경 로직이 복잡한 경우에 유용한 훅이다.
useState보다 더 정교한 상태 관리를 할 수 있으며, 주로 액션을 기반으로 상태를 업데이트하는 방식으로 동작한다.
import React, { useReducer } from 'react';
// 초기 상태
const initialState = { count: 0 };
// 상태를 업데이트하는 reducer 함수
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}
function Counter() {
// useReducer 훅을 사용하여 상태와 dispatch 함수 반환
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
}
export default Counter;
useReducer는 상태 state와 상태를 변경하는 함수 dispatch를 반환한다.
dispatch({ type: 'increment' })를 호출하면 reducer가 실행되어 count가 1 증가하고,
dispatch({ type: 'decrement' })를 호출하면 count가 1 감소한다.
dispatch는 액션 객체를 reducer로 전달하여 상태를 변경하게 하는 함수
- dispatch는 type과 그에 해당하는 payload를 포함하는 액션 객체를 인수로 받아서 실행됨.
- reducer 함수는 이 액션 객체를 받아 상태를 갱신
유의할점
리액트 내부에서 제공하는 훅함수 말고도 직접 정의해서 커스텀 훅을 만들 수 있다.
기본적으로 use로 시작하는 함수여야 하며, 리액트 훅을 내부에서 호출하여 상태를 관리하거나 다른 훅의 로직을 재사용할 수 있다.
커스텀 훅 작성 방법
1. 커스텀 훅은 리액트 훅을 내부에서 호출한다.
2. 상태나 로직을 반환한다.
3. use로 시작하는 함수명으로 작성한다.
import { useState } from 'react';
// 카운트 관리하는 커스텀 훅
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
return { count, increment, decrement };
}
export default useCounter;
커스텀 훅 내부에서 다른 훅을 호출하여 커스텀 할 수 있다.