React 요약 4 - 생명주기와 Hooks
Updated:
주요 Hooks
useEffect
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// 컴포넌트 마운트 시 또는 userId가 변경될 때 실행
useEffect(() => {
setLoading(true);
fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json())
.then(data => {
setUser(data);
setLoading(false);
})
.catch(error => {
console.error('Error fetching user:', error);
setLoading(false);
});
// 클린업 함수 (언마운트 시 또는 의존성 변경 전에 실행)
return () => {
console.log('Cleaning up...');
// 예: 타이머 정리, 구독 취소 등
};
}, [userId]); // 의존성 배열
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
코멘트: useEffect는 함수형 컴포넌트에서 side effects를 수행하기 위한 Hook입니다. 의존성 배열에 따라 다양한 생명주기 메서드를 대체할 수 있습니다:
- 빈 배열([])을 전달하면 componentDidMount와 유사하게 마운트 시 한 번만 실행
- 의존성 배열에 값을 넣으면 해당 값이 변경될 때마다 실행 (componentDidUpdate와 유사)
- 클린업 함수를 반환하면 componentWillUnmount와 유사하게 언마운트 시 실행
useEffect 사용 패턴
// 1. 마운트 시 한 번만 실행 (componentDidMount)
useEffect(() => {
console.log('Component mounted');
// 초기 데이터 로딩, 이벤트 리스너 등록 등
return () => {
console.log('Component will unmount');
// 정리 작업
};
}, []);
// 2. 특정 값이 변경될 때마다 실행 (componentDidUpdate)
useEffect(() => {
console.log('count changed to:', count);
}, [count]);
// 3. 모든 렌더링 후 실행 (의존성 배열 생략)
useEffect(() => {
console.log('Component rendered');
});
코멘트: useEffect의 의존성 배열을 올바르게 관리하는 것이 중요합니다. 빈 배열을 사용하면 마운트 시에만 실행되고, 배열을 생략하면 모든 렌더링 후에 실행됩니다. 의존성 배열에 포함된 값이 변경될 때만 효과가 다시 실행됩니다.
useRef
import { useRef, useEffect } from 'react';
function AutoFocusInput() {
// DOM 요소에 접근하기 위한 ref
const inputRef = useRef(null);
// 마운트 시 input에 포커스
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
function Timer() {
// 렌더링을 트리거하지 않고 값을 저장하기 위한 ref
const timerIdRef = useRef(null);
const [count, setCount] = useState(0);
const startTimer = () => {
if (timerIdRef.current) return; // 이미 타이머가 실행 중이면 무시
timerIdRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
};
const stopTimer = () => {
clearInterval(timerIdRef.current);
timerIdRef.current = null;
};
// 컴포넌트 언마운트 시 타이머 정리
useEffect(() => {
return () => {
if (timerIdRef.current) {
clearInterval(timerIdRef.current);
}
};
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={startTimer}>Start</button>
<button onClick={stopTimer}>Stop</button>
</div>
);
}
코멘트: useRef는 두 가지 주요 용도가 있습니다:
- DOM 요소에 직접 접근할 때 (focus, 측정 등)
- 렌더링을 트리거하지 않고 값을 저장할 때 (타이머 ID, 이전 값 등)
ref.current의 변경은 렌더링을 유발하지 않으므로 렌더링 사이클과 관련 없는 값을 저장하는 데 적합합니다.
useMemo와 useCallback
import { useState, useMemo, useCallback } from 'react';
function ExpensiveCalculation({ list, filter }) {
// 계산 결과를 메모이제이션 (의존성이 변경될 때만 재계산)
const filteredList = useMemo(() => {
console.log('Filtering list...');
return list.filter(item => item.includes(filter));
}, [list, filter]); // list나 filter가 변경될 때만 재계산
// 함수를 메모이제이션 (의존성이 변경될 때만 새 함수 생성)
const handleItemClick = useCallback((item) => {
console.log('Item clicked:', item);
// 처리 로직...
}, []); // 의존성이 없으므로 컴포넌트가 리렌더링되어도 함수는 유지됨
return (
<ul>
{filteredList.map(item => (
<li key={item} onClick={() => handleItemClick(item)}>
{item}
</li>
))}
</ul>
);
}
코멘트:
- useMemo는 계산 비용이 많이 드는 값을 메모이제이션하여 불필요한 재계산을 방지합니다.
- useCallback은 함수를 메모이제이션하여 불필요한 재생성을 방지합니다. 특히 자식 컴포넌트에 props로 함수를 전달할 때 유용합니다.
- 두 Hook 모두 성능 최적화를 위한 것이므로 모든 계산이나 함수에 사용할 필요는 없습니다. 실제로 성능 문제가 있는 경우에만 사용하는 것이 좋습니다.
useReducer
import { useReducer } from 'react';
// 리듀서 함수 정의
function counterReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'RESET':
return { count: 0 };
case 'SET':
return { count: action.payload };
default:
throw new Error(`Unsupported action type: ${action.type}`);
}
}
function Counter() {
// [현재 상태, 액션을 디스패치하는 함수] = useReducer(리듀서 함수, 초기 상태)
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
<button onClick={() => dispatch({ type: 'SET', payload: 10 })}>Set to 10</button>
</div>
);
}
코멘트: useReducer는 복잡한 상태 로직을 관리할 때 useState의 대안으로 사용됩니다. Redux와 유사한 패턴으로 상태 업데이트 로직을 컴포넌트 외부로 분리할 수 있습니다. 특히 다음과 같은 경우에 유용합니다:
- 여러 하위 값을 포함하는 복잡한 상태 객체
- 이전 상태에 의존하는 상태 업데이트
- 상태 업데이트 로직이 복잡한 경우
커스텀 Hooks
// 커스텀 Hook 정의
function useLocalStorage(key, initialValue) {
// 초기 상태 설정
const [storedValue, setStoredValue] = useState(() => {
try {
// localStorage에서 값 가져오기
const item = window.localStorage.getItem(key);
// 저장된 값이 있으면 파싱, 없으면 초기값 사용
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// 값을 설정하고 localStorage에 저장하는 함수
const setValue = (value) => {
try {
// 함수로 전달된 경우 처리
const valueToStore = value instanceof Function ? value(storedValue) : value;
// 상태 업데이트
setStoredValue(valueToStore);
// localStorage에 저장
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// 커스텀 Hook 사용
function App() {
const [name, setName] = useLocalStorage('name', 'Guest');
return (
<div>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="Enter your name"
/>
<p>Hello, {name}!</p>
</div>
);
}
코멘트: 커스텀 Hook은 로직을 재사용 가능한 함수로 추출하는 방법입니다. 이름이 “use”로 시작하는 함수로, 내부에서 다른 Hook을 사용할 수 있습니다. 커스텀 Hook을 사용하면 컴포넌트 간에 상태 로직을 공유할 수 있으며, 코드를 더 모듈화하고 테스트하기 쉽게 만들 수 있습니다.
Leave a comment