[이제와서 시작하는 React 마스터하기 #5] useEffect - 컴포넌트 생명주기
이 포스트를 읽고 나면 ✅ useEffect가 무엇인지 이해할 수 있어요 ✅ 컴포넌트가 나타날 때/사라질 때 코드를 실행할 수 있어요 ✅ 데이터를 불러오고 타이머를 다룰 수 있어요
📚 시작하기 전에
이전 포스트 Day 4: Props에서 컴포넌트끼리 대화하는 방법을 배웠어요.
이제 새로운 질문이 생겨요:
“컴포넌트가 화면에 나타난 후에 뭔가를 하고 싶다면?”
예를 들어:
- 서버에서 데이터 불러오기 📡
- 타이머 시작하기 ⏰
- 브라우저 제목 바꾸기 📄
- 스크롤 이벤트 감지하기 📜
이런 작업들을 Side Effect(부수 효과)라고 해요. 그리고 이걸 다루는 Hook이 바로 useEffect예요!
🤔 useEffect가 뭐예요?
useEffect는 컴포넌트의 생명주기에 관여할 수 있게 해주는 Hook이에요.
생명주기가 뭐예요?
컴포넌트도 사람처럼 태어나고 → 살다가 → 사라져요!
1
2
3
4
5
6
7
8
9
10
컴포넌트 생명주기:
1. 태어남 (Mount)
└─ 컴포넌트가 화면에 처음 나타남
2. 살아감 (Update)
└─ Props나 State가 바뀌면 다시 그려짐
3. 사라짐 (Unmount)
└─ 컴포넌트가 화면에서 제거됨
useEffect는 이 각 시점에 코드를 실행할 수 있게 해줘요!
🎯 useEffect 첫 만남
가장 간단한 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { useState, useEffect } from 'react';
function App() {
const [count, setCount] = useState(0);
// useEffect 사용하기
useEffect(() => {
console.log('컴포넌트가 렌더링됐어요!');
});
return (
<div style=>
<h1>카운트: {count}</h1>
<button onClick={() => setCount(count + 1)}>
증가
</button>
</div>
);
}
export default App;
실행해보기
- 파일을 저장하세요
- 브라우저의 콘솔(F12)을 열어보세요
- 버튼을 클릭해보세요
콘솔에 뭐가 찍히나요?
버튼을 클릭할 때마다 “컴포넌트가 렌더링됐어요!”가 출력돼요!
코드 이해하기
1
2
3
useEffect(() => {
console.log('여기 코드가 실행돼요!');
});
() => { ... }: 실행할 함수- 이 함수는 컴포넌트가 화면에 그려질 때마다 실행돼요
💡 useEffect의 핵심: 의존성 배열
useEffect의 두 번째 인자로 의존성 배열(dependency array)을 줄 수 있어요.
패턴 1: 배열 없음 (매번 실행)
1
2
3
4
5
6
useEffect(() => {
console.log('렌더링될 때마다 실행!');
});
// 렌더링 → 실행
// 렌더링 → 실행
// 렌더링 → 실행 ...
패턴 2: 빈 배열 [] (한 번만 실행)
1
2
3
4
5
6
useEffect(() => {
console.log('컴포넌트가 처음 나타날 때만 실행!');
}, []); // ← 빈 배열!
// Mount → 실행
// Update → 실행 안 함
// Update → 실행 안 함 ...
패턴 3: 특정 값 [count] (값이 바뀔 때만 실행)
1
2
3
4
5
useEffect(() => {
console.log('count가 바뀔 때만 실행!');
}, [count]); // ← count를 넣음!
// count 변경 → 실행
// 다른 state 변경 → 실행 안 함
💪 실습 1: 브라우저 제목 바꾸기
가장 간단한 실습부터 해봐요!
목표
카운트가 바뀔 때마다 브라우저 탭의 제목을 바꿔봐요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { useState, useEffect } from 'react';
function App() {
const [count, setCount] = useState(0);
// count가 바뀔 때마다 실행
useEffect(() => {
document.title = `카운트: ${count}`;
}, [count]);
return (
<div style=>
<h1>카운트: {count}</h1>
<button onClick={() => setCount(count + 1)}>
증가
</button>
</div>
);
}
export default App;
결과 확인
버튼을 클릭하면 브라우저 탭의 제목이 바뀌어요! 🎉
왜 그럴까요?
- 버튼 클릭 →
count변경 count가 의존성 배열에 있으니 useEffect 실행document.title변경!
💪 실습 2: 타이머 만들기
이번엔 1초마다 자동으로 증가하는 타이머를 만들어봐요!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { useState, useEffect } from 'react';
function App() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// 1초마다 실행
const timer = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// 정리 함수 (cleanup)
return () => {
clearInterval(timer);
console.log('타이머 정리됨');
};
}, []); // 빈 배열: 컴포넌트가 처음 나타날 때만 설정
return (
<div style=>
<h1>경과 시간: {seconds}초</h1>
</div>
);
}
export default App;
새로운 개념: Cleanup 함수
1
2
3
4
5
6
7
8
9
useEffect(() => {
// 설정 코드
const timer = setInterval(...);
// 정리 코드 (return으로!)
return () => {
clearInterval(timer);
};
}, []);
왜 정리가 필요할까요?
타이머를 계속 돌려두면:
- 메모리 낭비 💸
- 성능 저하 🐌
- 버그 발생 🐛
컴포넌트가 사라질 때 타이머도 멈춰야 해요!
정리 함수는 언제 실행될까요?
- 컴포넌트가 사라질 때 (Unmount)
- useEffect가 다시 실행되기 전
💪 실습 3: 데이터 불러오기
실전에서 가장 많이 쓰는 패턴이에요!
목표
컴포넌트가 나타나면 서버에서 데이터를 불러와요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { useState, useEffect } from 'react';
function App() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 데이터 불러오기
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(response => response.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, []); // 한 번만 실행
if (loading) {
return <div style=>로딩 중...</div>;
}
return (
<div style=>
<h1>사용자 정보</h1>
<p>이름: {user.name}</p>
<p>이메일: {user.email}</p>
<p>전화: {user.phone}</p>
</div>
);
}
export default App;
더 좋은 방법: async/await 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { useState, useEffect } from 'react';
function App() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// async 함수 만들기
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
const data = await response.json();
setUser(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
}, []);
if (loading) return <div>로딩 중...</div>;
if (error) return <div>에러: {error}</div>;
return (
<div style=>
<h1>사용자 정보</h1>
<p>이름: {user.name}</p>
<p>이메일: {user.email}</p>
<p>전화: {user.phone}</p>
</div>
);
}
export default App;
💪 실습 4: 검색 기능 만들기
입력값이 바뀔 때마다 데이터를 검색해봐요!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { useState, useEffect } from 'react';
function App() {
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
// 검색어가 없으면 실행하지 않음
if (!searchTerm) {
setResults([]);
return;
}
// 검색 실행
const search = async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users?name_like=${searchTerm}`
);
const data = await response.json();
setResults(data);
};
search();
}, [searchTerm]); // searchTerm이 바뀔 때마다 실행
return (
<div style=>
<h1>사용자 검색</h1>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="이름을 입력하세요"
style=
/>
<div style=>
{results.map(user => (
<div
key={user.id}
style=
>
<strong>{user.name}</strong>
<p>{user.email}</p>
</div>
))}
</div>
{searchTerm && results.length === 0 && (
<p>검색 결과가 없어요.</p>
)}
</div>
);
}
export default App;
⚠️ 자주 하는 실수
실수 1: 무한 루프
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const [count, setCount] = useState(0);
// ❌ 무한 루프!
useEffect(() => {
setCount(count + 1); // count 변경
}); // 의존성 배열 없음 → 렌더링할 때마다 실행 → count 변경 → 렌더링 → ...
// ✅ 해결책 1: 의존성 배열 추가
useEffect(() => {
setCount(count + 1);
}, []); // 한 번만 실행
// ✅ 해결책 2: 조건문 사용
useEffect(() => {
if (count < 10) {
setCount(count + 1);
}
}, [count]);
실수 2: 의존성 배열에 빠뜨리기
1
2
3
4
5
6
7
8
9
10
11
12
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// ❌ name을 사용하는데 의존성 배열에 없음!
useEffect(() => {
console.log(`${name}님의 카운트: ${count}`);
}, [count]); // name이 빠짐!
// ✅ 올바른 방법
useEffect(() => {
console.log(`${name}님의 카운트: ${count}`);
}, [count, name]); // 둘 다 넣기!
VS Code가 경고를 줘요! 경고를 잘 읽고 따라가세요.
실수 3: 정리 함수 잊어버리기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ❌ 정리 안 함 → 메모리 누수!
useEffect(() => {
const timer = setInterval(() => {
console.log('실행 중...');
}, 1000);
// 정리 함수 없음!
}, []);
// ✅ 올바른 방법
useEffect(() => {
const timer = setInterval(() => {
console.log('실행 중...');
}, 1000);
return () => {
clearInterval(timer); // 정리!
};
}, []);
🎯 종합 실습: 날씨 앱 만들기
배운 내용을 모두 활용해서 간단한 날씨 앱을 만들어봐요!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import { useState, useEffect } from 'react';
function App() {
const [city, setCity] = useState('Seoul');
const [weather, setWeather] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchWeather = async () => {
setLoading(true);
// 실제로는 API 키가 필요해요
// 여기서는 예시 데이터를 사용해요
setTimeout(() => {
const mockData = {
Seoul: { temp: 15, condition: '맑음' },
Busan: { temp: 18, condition: '흐림' },
Jeju: { temp: 20, condition: '비' }
};
setWeather(mockData[city]);
setLoading(false);
}, 1000);
};
fetchWeather();
}, [city]); // city가 바뀔 때마다 실행
return (
<div style=>
<h1>날씨 앱 🌤️</h1>
<select
value={city}
onChange={(e) => setCity(e.target.value)}
style=
>
<option value="Seoul">서울</option>
<option value="Busan">부산</option>
<option value="Jeju">제주</option>
</select>
{loading && <div>날씨 정보 불러오는 중...</div>}
{!loading && weather && (
<div
style=
>
<h2>{city}</h2>
<p style=>🌡️ {weather.temp}°C</p>
<p style=>{weather.condition}</p>
</div>
)}
</div>
);
}
export default App;
📝 정리
자, Day 5를 마무리할 시간이에요!
오늘 배운 내용 체크리스트
- useEffect가 무엇인지 이해했어요
- 의존성 배열의 역할을 알아요
- 정리 함수(cleanup)를 사용할 수 있어요
- 데이터를 불러올 수 있어요
- 타이머를 다룰 수 있어요
핵심 요약 3줄
- useEffect: 컴포넌트 생명주기에서 코드를 실행하는 Hook
- 의존성 배열:
[]빈 배열은 한 번만,[count]값이 바뀔 때마다 - 정리 함수:
return () => {...}로 타이머나 이벤트 정리
💪 숙제 (선택사항)
- 시계 만들기
- 현재 시간을 1초마다 업데이트해서 보여주세요
- 시, 분, 초를 모두 표시하세요
- todo 앱에 localStorage 연동
- Day 3의 Todo 앱에 useEffect 추가
- 로딩할 때 localStorage에서 불러오기
- 변경될 때마다 localStorage에 저장하기
- 검색 디바운싱
- 검색어 입력 후 0.5초 뒤에 검색 실행
- 타이핑 중에는 검색하지 않게 만들기
🚀 다음 단계
축하합니다! useEffect를 마스터했어요! 🎉
이제 컴포넌트의 생명주기를 다룰 수 있어요. 데이터도 불러오고, 타이머도 만들고!
다음 포스트에서는 조건부 렌더링과 리스트 렌더링을 배워서 더 동적이고 유연한 UI를 만들어볼 거예요!
➡️ Day 6: 조건부 렌더링과 리스트에서 만나요!
💬 useEffect가 어렵게 느껴지나요?
처음엔 모두 그래요! “왜 의존성 배열이 필요해?” “정리 함수는 언제 써?”
핵심만 기억하세요:
- 빈 배열 []: 컴포넌트가 처음 나타날 때 한 번만
- [값]: 그 값이 바뀔 때마다
- 정리 함수: 타이머나 이벤트 리스너를 쓸 때 반드시!
몇 번 써보면 패턴이 보여요. 데이터 불러오기 예제를 직접 타이핑하면서 익혀보세요! 💪
🎓 시리즈 목록
Phase 1: React 기초 (Day 1-8)
- React, 왜 배워야 할까요?
- 첫 React 앱 만들어보기
- useState - 화면을 움직이게 만들기
- Props - 컴포넌트끼리 대화하기
- useEffect - 컴포넌트 생명주기 (현재 포스트)
- 컴포넌트 생명주기 이해하기
- Context API로 전역 상태 관리하기
- Custom Hooks 만들기
Phase 2: React 활용 (Day 9-14)
Phase 3: React 심화 (Day 15-20)
🔗 유용한 리소스
- React 공식 문서 - useEffect - useEffect 완벽 가이드
- React 공식 문서 - Effect 동기화 - Effect 개념 이해
- JavaScript Promise - 비동기 처리
