포스트

[이제와서 시작하는 React 마스터하기 #5] useEffect - 컴포넌트 생명주기

[이제와서 시작하는 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;

실행해보기

  1. 파일을 저장하세요
  2. 브라우저의 콘솔(F12)을 열어보세요
  3. 버튼을 클릭해보세요

콘솔에 뭐가 찍히나요?

버튼을 클릭할 때마다 “컴포넌트가 렌더링됐어요!”가 출력돼요!

코드 이해하기

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;

결과 확인

버튼을 클릭하면 브라우저 탭의 제목이 바뀌어요! 🎉

왜 그럴까요?

  1. 버튼 클릭 → count 변경
  2. count가 의존성 배열에 있으니 useEffect 실행
  3. 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);
  };
}, []);

왜 정리가 필요할까요?

타이머를 계속 돌려두면:

  • 메모리 낭비 💸
  • 성능 저하 🐌
  • 버그 발생 🐛

컴포넌트가 사라질 때 타이머도 멈춰야 해요!

정리 함수는 언제 실행될까요?

  1. 컴포넌트가 사라질 때 (Unmount)
  2. 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줄

  1. useEffect: 컴포넌트 생명주기에서 코드를 실행하는 Hook
  2. 의존성 배열: [] 빈 배열은 한 번만, [count] 값이 바뀔 때마다
  3. 정리 함수: return () => {...}로 타이머나 이벤트 정리

💪 숙제 (선택사항)

  1. 시계 만들기
    • 현재 시간을 1초마다 업데이트해서 보여주세요
    • 시, 분, 초를 모두 표시하세요
  2. todo 앱에 localStorage 연동
    • Day 3의 Todo 앱에 useEffect 추가
    • 로딩할 때 localStorage에서 불러오기
    • 변경될 때마다 localStorage에 저장하기
  3. 검색 디바운싱
    • 검색어 입력 후 0.5초 뒤에 검색 실행
    • 타이핑 중에는 검색하지 않게 만들기

🚀 다음 단계

축하합니다! useEffect를 마스터했어요! 🎉

이제 컴포넌트의 생명주기를 다룰 수 있어요. 데이터도 불러오고, 타이머도 만들고!

다음 포스트에서는 조건부 렌더링과 리스트 렌더링을 배워서 더 동적이고 유연한 UI를 만들어볼 거예요!

➡️ Day 6: 조건부 렌더링과 리스트에서 만나요!


💬 useEffect가 어렵게 느껴지나요?

처음엔 모두 그래요! “왜 의존성 배열이 필요해?” “정리 함수는 언제 써?”

핵심만 기억하세요:

  • 빈 배열 []: 컴포넌트가 처음 나타날 때 한 번만
  • [값]: 그 값이 바뀔 때마다
  • 정리 함수: 타이머나 이벤트 리스너를 쓸 때 반드시!

몇 번 써보면 패턴이 보여요. 데이터 불러오기 예제를 직접 타이핑하면서 익혀보세요! 💪


🎓 시리즈 목록

Phase 1: React 기초 (Day 1-8)

  1. React, 왜 배워야 할까요?
  2. 첫 React 앱 만들어보기
  3. useState - 화면을 움직이게 만들기
  4. Props - 컴포넌트끼리 대화하기
  5. useEffect - 컴포넌트 생명주기 (현재 포스트)
  6. 컴포넌트 생명주기 이해하기
  7. Context API로 전역 상태 관리하기
  8. Custom Hooks 만들기

Phase 2: React 활용 (Day 9-14)

  1. React Router로 SPA 만들기
  2. 폼과 유효성 검사
  3. 성능 최적화 기법
  4. 에러 처리와 경계
  5. Ref와 DOM 접근
  6. Portals와 고급 패턴

Phase 3: React 심화 (Day 15-20)

  1. TypeScript와 React
  2. 상태 관리 라이브러리
  3. 서버 상태 관리 (TanStack Query)
  4. 테스팅 전략
  5. SSR과 Next.js 기초
  6. 배포와 DevOps

🔗 유용한 리소스

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.