컴포넌트 생명주기와 useEffect
3. 컴포넌트 생명주기와 useEffect
React 컴포넌트는 생성되고, 업데이트되고, 제거되는 생명주기를 가집니다. useEffect Hook을 사용하면 이러한 생명주기에 맞춰 코드를 실행할 수 있습니다. 데이터 가져오기, 타이머 설정, 정리 작업 등을 학습해봅시다.
useEffect 기본 사용법
처음 한 번만 실행하기
import { useState, useEffect } from 'react';
function WelcomeMessage() {
const [user, setUser] = useState('');
// 컴포넌트가 처음 렌더링될 때 한 번만 실행
useEffect(() => {
console.log('컴포넌트가 마운트되었습니다');
setUser('김개발');
}, []); // 빈 배열 = 한 번만 실행
return (
<div>
<h3>환영 메시지</h3>
<p>안녕하세요, {user}님!</p>
</div>
);
}
상태가 변할 때마다 실행하기
import { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [message, setMessage] = useState('');
// count가 변할 때마다 실행
useEffect(() => {
console.log(`카운트가 ${count}로 변경됨`);
if (count === 0) {
setMessage('시작!');
} else if (count > 0 && count < 5) {
setMessage('계속 클릭해보세요');
} else if (count >= 5) {
setMessage('많이 클릭했네요!');
}
}, [count]); // count가 변할 때마다 실행
return (
<div>
<h3>카운터</h3>
<p>카운트: {count}</p>
<p>메시지: {message}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
<button onClick={() => setCount(count - 1)}>감소</button>
<button onClick={() => setCount(0)}>초기화</button>
</div>
);
}
매 렌더링마다 실행하기
import { useState, useEffect } from 'react';
function RenderTracker() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// 매 렌더링마다 실행 (의존성 배열 없음)
useEffect(() => {
console.log('컴포넌트가 렌더링되었습니다');
document.title = `카운트: ${count}, 이름: ${name}`;
});
return (
<div>
<h3>렌더링 추적기</h3>
<p>카운트: {count}</p>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="이름을 입력하세요"
/>
<button onClick={() => setCount(count + 1)}>카운트 증가</button>
<p>브라우저 탭 제목을 확인해보세요!</p>
</div>
);
}
정리 함수 (Cleanup Function)
타이머 정리하기
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
let interval = null;
if (isRunning) {
interval = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
}
// 정리 함수: 컴포넌트가 언마운트되거나 의존성이 변경될 때 실행
return () => {
if (interval) {
clearInterval(interval);
console.log('타이머가 정리되었습니다');
}
};
}, [isRunning]);
const startTimer = () => setIsRunning(true);
const stopTimer = () => setIsRunning(false);
const resetTimer = () => {
setSeconds(0);
setIsRunning(false);
};
return (
<div>
<h3>타이머</h3>
<p>경과 시간: {seconds}초</p>
<button onClick={startTimer} disabled={isRunning}>시작</button>
<button onClick={stopTimer} disabled={!isRunning}>정지</button>
<button onClick={resetTimer}>초기화</button>
</div>
);
}
이벤트 리스너 정리하기
import { useState, useEffect } from 'react';
function WindowSizeTracker() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
// 이벤트 리스너 추가
window.addEventListener('resize', handleResize);
// 정리 함수: 이벤트 리스너 제거
return () => {
window.removeEventListener('resize', handleResize);
console.log('이벤트 리스너가 제거되었습니다');
};
}, []);
return (
<div>
<h3>창 크기 추적기</h3>
<p>창 너비: {windowSize.width}px</p>
<p>창 높이: {windowSize.height}px</p>
<p>창 크기를 변경해보세요!</p>
</div>
);
}
데이터 가져오기
API 호출 시뮬레이션
import { useState, useEffect } from 'react';
// 가짜 API 함수
function fetchUserData(userId) {
return new Promise((resolve) => {
setTimeout(() => {
const users = {
1: { id: 1, name: '김개발', email: 'kim@example.com' },
2: { id: 2, name: '이디자인', email: 'lee@example.com' },
3: { id: 3, name: '박데이터', email: 'park@example.com' }
};
resolve(users[userId] || null);
}, 1000); // 1초 지연
});
}
function UserProfile() {
const [userId, setUserId] = useState(1);
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
setError(null);
fetchUserData(userId)
.then(userData => {
if (userData) {
setUser(userData);
} else {
setError('사용자를 찾을 수 없습니다');
}
})
.catch(err => {
setError('데이터를 불러오는 중 오류가 발생했습니다');
})
.finally(() => {
setLoading(false);
});
}, [userId]);
return (
<div>
<h3>사용자 프로필</h3>
<div>
<label>사용자 ID: </label>
<select value={userId} onChange={(e) => setUserId(Number(e.target.value))}>
<option value={1}>1</option>
<option value={2}>2</option>
<option value={3}>3</option>
<option value={4}>4 (존재하지 않음)</option>
</select>
</div>
{loading && <p>로딩 중...</p>}
{error && <p>오류: {error}</p>}
{user && (
<div>
<p>이름: {user.name}</p>
<p>이메일: {user.email}</p>
</div>
)}
</div>
);
}
실시간 데이터 업데이트
import { useState, useEffect } from 'react';
function LiveClock() {
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
const timer = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
return () => clearInterval(timer);
}, []);
return (
<div>
<h3>실시간 시계</h3>
<p>현재 시간: {currentTime.toLocaleTimeString()}</p>
</div>
);
}
function RandomNumberGenerator() {
const [number, setNumber] = useState(0);
const [isGenerating, setIsGenerating] = useState(false);
useEffect(() => {
let interval = null;
if (isGenerating) {
interval = setInterval(() => {
setNumber(Math.floor(Math.random() * 100) + 1);
}, 500);
}
return () => {
if (interval) clearInterval(interval);
};
}, [isGenerating]);
return (
<div>
<h3>랜덤 숫자 생성기</h3>
<p>현재 숫자: {number}</p>
<button onClick={() => setIsGenerating(!isGenerating)}>
{isGenerating ? '정지' : '시작'}
</button>
</div>
);
}
조건부 useEffect
특정 조건에서만 실행하기
import { useState, useEffect } from 'react';
function ConditionalEffect() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const [saveMessage, setSaveMessage] = useState('');
// count가 5의 배수일 때만 실행
useEffect(() => {
if (count > 0 && count % 5 === 0) {
setSaveMessage(`카운트 ${count}에서 자동 저장됨`);
}
}, [count]);
// name이 3글자 이상일 때만 실행
useEffect(() => {
if (name.length >= 3) {
console.log(`유효한 이름: ${name}`);
setSaveMessage(`이름 '${name}'이 유효합니다`);
} else if (name.length > 0) {
setSaveMessage('이름은 3글자 이상이어야 합니다');
} else {
setSaveMessage('');
}
}, [name]);
return (
<div>
<h3>조건부 useEffect</h3>
<div>
<p>카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>증가</button>
<button onClick={() => setCount(0)}>초기화</button>
<p>카운트가 5의 배수가 되면 자동 저장됩니다</p>
</div>
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="이름을 입력하세요 (3글자 이상)"
/>
</div>
{saveMessage && <p>메시지: {saveMessage}</p>}
</div>
);
}
실습: 할 일 목록 with 로컬스토리지
import { useState, useEffect } from 'react';
function TodoAppWithStorage() {
const [todos, setTodos] = useState([]);
const [inputText, setInputText] = useState('');
// 컴포넌트 마운트 시 로컬스토리지에서 데이터 불러오기
useEffect(() => {
const savedTodos = localStorage.getItem('todos');
if (savedTodos) {
try {
setTodos(JSON.parse(savedTodos));
} catch (error) {
console.error('저장된 할 일 목록을 불러오는 중 오류 발생:', error);
}
}
}, []);
// todos가 변경될 때마다 로컬스토리지에 저장
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
const addTodo = () => {
if (inputText.trim()) {
const newTodo = {
id: Date.now(),
text: inputText.trim(),
completed: false
};
setTodos([...todos, newTodo]);
setInputText('');
}
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const clearCompleted = () => {
setTodos(todos.filter(todo => !todo.completed));
};
return (
<div>
<h3>할 일 목록 (자동 저장)</h3>
<div>
<input
type="text"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="할 일을 입력하세요"
/>
<button onClick={addTodo}>추가</button>
</div>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>
{todo.text}
</span>
</label>
<button onClick={() => deleteTodo(todo.id)}>삭제</button>
</li>
))}
</ul>
{todos.length > 0 && (
<div>
<p>총 {todos.length}개, 완료 {todos.filter(t => t.completed).length}개</p>
<button onClick={clearCompleted}>완료된 항목 삭제</button>
</div>
)}
</div>
);
}
useEffect 사용 시 주의사항
무한 루프 방지
import { useState, useEffect } from 'react';
function InfiniteLoopExample() {
const [count, setCount] = useState(0);
const [data, setData] = useState([]);
// ❌ 잘못된 예: 무한 루프 발생
// useEffect(() => {
// setCount(count + 1); // count 변경
// }, [count]); // count가 변경될 때마다 실행 → 무한 루프
// ✅ 올바른 예 1: 특정 조건에서만 업데이트
useEffect(() => {
if (count < 5) {
setCount(count + 1);
}
}, [count]);
// ❌ 잘못된 예: 객체를 의존성에 직접 포함
// useEffect(() => {
// // 일부 로직
// }, [data]); // 배열이나 객체는 참조가 매번 달라질 수 있음
// ✅ 올바른 예 2: 객체의 특정 속성만 의존성에 포함
useEffect(() => {
console.log('데이터 길이가 변경됨:', data.length);
}, [data.length]);
return (
<div>
<h3>useEffect 주의사항</h3>
<p>카운트: {count} (5에서 멈춤)</p>
<p>데이터 개수: {data.length}</p>
<button onClick={() => setData([...data, Date.now()])}>
데이터 추가
</button>
</div>
);
}
정리
useEffect와 컴포넌트 생명주기의 핵심 개념을 학습했습니다:
- 기본 useEffect: 마운트, 업데이트, 언마운트 시점에 코드 실행
- 의존성 배열: 빈 배열, 특정 값, 배열 없음의 차이점
- 정리 함수: 타이머, 이벤트 리스너 등의 정리 작업
- 데이터 가져오기: API 호출과 로딩 상태 관리
- 조건부 실행: 특정 조건에서만 useEffect 실행
- 실습: 로컬스토리지를 활용한 데이터 영속성
- 주의사항: 무한 루프 방지와 올바른 의존성 배열 사용
useEffect는 React에서 부수 효과(side effects)를 처리하는 핵심 Hook입니다. 적절한 의존성 배열과 정리 함수를 사용하면 메모리 누수 없이 효율적인 React 애플리케이션을 만들 수 있습니다.
다음 장에서는 사용자 정의 Hook에 대해 알아보겠습니다.