← 돌아가기

Step 5: useEffect 기초

1️⃣ 순수 HTML + JavaScript

먼저 우리가 익숙한 방식으로 1초마다 증가하는 타이머를 만들어봅시다.

<!DOCTYPE html>
<html>
<body>
  <div id="display">0</div>

  <script>
    let count = 0;
    const display = document.querySelector("#display");
    
    // 1초마다 실행
    setInterval(() => {
      count++;
      display.textContent = count;
    }, 1000);
  </script>
</body>
</html>

✅ 이 방식은 잘 작동합니다! Session 4까지는 이렇게 했었죠.

2️⃣ React로 변환 (❌ 문제 발생!)

같은 코드를 React로 옮기면... 문제가 생깁니다!

"use client";
import { useState } from "react";

export default function Timer() {
  const [count, setCount] = useState(0);
  
  // ❌ 이렇게 하면 안 됨!
  setInterval(() => {
    setCount(count + 1);
  }, 1000);
  
  return <div>{count}</div>;
}

💥 무한 렌더링 발생!

  • 1. 컴포넌트가 렌더링됨
  • 2. setInterval이 실행됨 (타이머 시작)
  • 3. 상태가 변경되면 다시 렌더링
  • 4. 또 setInterval 실행 (타이머가 또 시작!)
  • 5. 무한 반복... 💥

3️⃣ useEffect로 해결! ✅

타이머는 useEffect 안에 넣어야 합니다.

"use client";
import { useState, useEffect } from "react";

export default function Timer() {
  const [count, setCount] = useState(0);
  
  // ✅ useEffect 안에 넣기!
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(prev => prev + 1);
    }, 1000);
    
    // 정리: 컴포넌트 사라질 때 타이머 멈추기
    return () => clearInterval(timer);
  }, []); // ← 빈 배열: 처음 한 번만 실행
  
  return (
    <div className="text-5xl font-bold text-center p-8">
      {count}
    </div>
  );
}

✅ 완벽하게 작동!

  • • 타이머가 한 번만 시작됨
  • • 1초마다 정확히 증가
  • • 컴포넌트가 사라지면 타이머도 정리됨

📖 useEffect 문법

useEffect(() => {
  // 실행할 코드
  
  return () => {
    // 정리 코드 (선택사항)
  };
}, []); // ← 빈 배열: 처음 한 번만 실행

🎯 핵심 규칙

  • 타이머, API 호출 등은 useEffect 안에 넣기
  • • 끝에 [] 붙이기 → 처음 한 번만 실행
  • return () => {} → 정리 작업 (타이머 멈추기 등)

💡 왜 []를 붙이나요?

빈 배열 []은 "이 코드를 처음 한 번만 실행하세요"라는 의미입니다.
만약 없으면 매번 렌더링할 때마다 실행되어 문제가 생깁니다.

📊 Before & After

순수 JavaScript

<script>
  let count = 0;
  
  setInterval(() => {
    count++;
    display.textContent = count;
  }, 1000);
</script>

React + useEffect

useEffect(() => {
  const timer = setInterval(() => {
    setCount(prev => prev + 1);
  }, 1000);
  
  return () => clearInterval(timer);
}, []);

💡 핵심 정리

✅ 기억할 것

  • • 타이머는 useEffect 안에
  • • 끝에 [] 붙이기
  • return () => clearInterval()로 정리

⚠️ 주의할 것

  • • 컴포넌트 본문에 직접 타이머 넣지 말기
  • [] 빠뜨리면 무한 렌더링!

💻 실습 1: 페이지 제목 동기화

카운터 값이 변경되면 브라우저 탭 제목도 함께 변경

"use client";
import { useState, useEffect } from "react";

export default function TitleSync() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    document.title = `카운트: ${count}`;
  }, [count]); // count 변경 시 실행
  
  return (
    <div className="flex flex-col gap-4 p-8">
      <div className="text-3xl font-bold text-center">{count}</div>
      <div className="flex gap-2 justify-center">
        <button 
          onClick={() => setCount(count - 1)}
          className="px-4 py-2 bg-red-500 text-white rounded"
        >
          -
        </button>
        <button 
          onClick={() => setCount(count + 1)}
          className="px-4 py-2 bg-green-500 text-white rounded"
        >
          +
        </button>
      </div>
      <p className="text-center text-gray-600 text-sm">
        👆 버튼을 누르고 브라우저 탭을 확인하세요!
      </p>
    </div>
  );
}

🎯 동작 방식

💻 실습 2: 자동 타이머

1초마다 자동으로 카운트 증가

"use client";
import { useState, useEffect } from "react";

export default function AutoTimer() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    // 1초마다 실행
    const timer = setInterval(() => {
      setCount(prev => prev + 1);
    }, 1000);
    
    // ⭐ cleanup: 컴포넌트 언마운트 시 타이머 정리
    return () => clearInterval(timer);
  }, []); // 빈 배열: 처음 한 번만 실행
  
  return (
    <div className="flex flex-col gap-4 p-8">
      <div className="text-5xl font-bold text-center">{count}</div>
      <p className="text-center text-gray-600">
        자동으로 증가 중... ⏰
      </p>
    </div>
  );
}

🧹 Cleanup 함수

useEffect에서 return하는 함수는 "정리" 작업을 수행합니다.

💻 실습 3: localStorage 연동

카운트 값을 브라우저에 저장하고 불러오기

"use client";
import { useState, useEffect } from "react";

export default function PersistentCounter() {
  const [count, setCount] = useState(0);
  
  // 처음 마운트될 때: localStorage에서 불러오기
  useEffect(() => {
    const saved = localStorage.getItem("count");
    if (saved) {
      setCount(Number(saved));
    }
  }, []); // 빈 배열: 처음 한 번만
  
  // count 변경될 때: localStorage에 저장
  useEffect(() => {
    localStorage.setItem("count", count);
  }, [count]); // count 변경 감지
  
  return (
    <div className="flex flex-col gap-4 p-8">
      <div className="text-4xl font-bold text-center">{count}</div>
      <div className="flex gap-2 justify-center">
        <button 
          onClick={() => setCount(count - 1)}
          className="px-4 py-2 bg-red-500 text-white rounded"
        >
          -
        </button>
        <button 
          onClick={() => setCount(0)}
          className="px-4 py-2 bg-gray-500 text-white rounded"
        >
          초기화
        </button>
        <button 
          onClick={() => setCount(count + 1)}
          className="px-4 py-2 bg-green-500 text-white rounded"
        >
          +
        </button>
      </div>
      <p className="text-center text-gray-600 text-sm">
        💾 페이지를 새로고침해도 값이 유지됩니다!
      </p>
    </div>
  );
}

💡 두 개의 useEffect

📊 Before & After

Session 4 (순수 JS)

// DOM 로드 후 실행
document
  .addEventListener(
    "DOMContentLoaded", 
    () => {
      // 타이머 시작
      setInterval(() => {
        count++;
        display.textContent = count;
      }, 1000);
    }
  );

Session 5 (React)

// 컴포넌트 마운트 후 실행
useEffect(() => {
  const timer = setInterval(() => {
    setCount(prev => prev + 1);
  }, 1000);
  
  return () => clearInterval(timer);
}, []);

💡 핵심 정리

🎯 useEffect를 쓰는 이유

  • • 컴포넌트 본문에 부수 효과 코드를 넣으면 무한 렌더링 위험
  • • useEffect로 실행 시점을 명확하게 제어
  • • 의존성 배열로 언제 실행할지 결정

📌 주요 사용 사례

  • • 타이머, 인터벌
  • • DOM 직접 조작 (document.title 등)
  • • localStorage, sessionStorage
  • • API 호출 (다음 세션에서 학습)
  • • 이벤트 리스너 등록/제거

⚠️ 주의사항

  • • 의존성 배열을 정확하게 지정하기
  • • cleanup 함수로 리소스 정리하기
  • • 무한 루프 주의 (의존성 배열 빠뜨리면 위험!)