[2편] React useEffect 완벽 가이드 - Todo 앱으로 배우는 생명주기와 CRUD
React useEffect 완벽 가이드 - Todo 앱으로 배우는 생명주기와 CRUD
React 공부하다가 useEffect 때문에 머리 아프셨죠?
처음에 저도 그랬어요. “의존성 배열이 뭐야?”, “왜 내 코드는 무한 루프에 빠지는 거야?”라고 소리치면서 모니터를 노려봤던 기억이 나네요. 콘솔창에서 에러가 1초에 수백 개씩 찍힐 때의 그 당황스러움… 지금 생각해도 아찔합니다.
하지만 걱정 마세요. 오늘 이 글 하나면 useEffect를 완벽하게 정복할 수 있습니다. 이론만 주구장창 설명하는 게 아니라, 실제로 Todo 앱을 만들면서 CRUD(생성, 조회, 수정, 삭제)까지 구현해볼 거예요. 코드 한 줄 한 줄 왜 이렇게 써야 하는지, 제 실수담과 함께 친절하게 알려드릴게요!
🔄 왜 useEffect를 배워야 할까? - 컴포넌트의 생명주기
본격적으로 들어가기 전에, 왜 useEffect가 필요한지부터 이해해야 합니다. React 컴포넌트는 사람의 인생처럼 태어나고, 살다가, 사라지는 주기를 가지고 있어요.
graph LR
A[Mount<br/>탄생] --> B[Update<br/>변화]
B --> B
B --> C[Unmount<br/>소멸]
style A fill:#a8e6cf
style B fill:#7ec8e3
style C fill:#ff8b94
1. Mount (마운트) - 컴포넌트의 탄생 🐣
컴포넌트가 처음으로 화면에 나타나는 순간입니다. 마치 우리가 세상에 태어나는 것처럼요. 이때 필요한 데이터를 서버에서 가져오거나, 초기 설정을 해야 할 때가 있죠.
2. Update (업데이트) - 컴포넌트의 변화 🔄
State나 Props가 바뀌면 컴포넌트가 다시 렌더링됩니다. 예를 들어, 사용자가 버튼을 클릭해서 count 값이 증가하면 화면이 업데이트되는 거죠. 이 과정이 앱이 살아있는 동안 계속 반복됩니다.
3. Unmount (언마운트) - 컴포넌트의 소멸 💀
컴포넌트가 화면에서 사라지는 순간입니다. 다른 페이지로 이동하거나, 조건부 렌더링으로 컴포넌트가 제거될 때죠. 이때는 타이머를 정리하거나 구독을 해제하는 등의 “뒷정리”가 필요합니다.
💡 실생활 비유로 이해하기
Mount: 아침에 일어나서 하루를 시작 (초기 설정)
Update: 하루 종일 밥 먹고, 일하고, 쉬는 등 활동 (상태 변화)
Unmount: 밤에 자기 전 양치하고 정리 (클린업)
⚡ useEffect, 이렇게 쓰는 겁니다
자, 이제 본격적으로 useEffect 사용법을 알아볼까요? useEffect는 언제 실행될지를 제어할 수 있는 의존성 배열(Dependency Array)이라는 게 있어요. 이게 핵심입니다!
기본 문법은 이렇게 생겼어요:
useEffect(콜백함수, [의존성배열]);
두 번째 인자인 의존성 배열에 뭘 넣느냐에 따라 실행 시점이 완전히 달라집니다. 하나씩 살펴볼게요.
graph TD
A[useEffect 실행 시점] --> B{의존성 배열?}
B -->|없음| C[렌더링 될 때마다<br/>항상 실행]
B -->|"빈 배열 []"| D[마운트 시<br/>단 1회만 실행]
B -->|"[count]"| E[count 변경 시<br/>실행]
B -->|return 함수| F[언마운트 시<br/>클린업 실행]
style C fill:#ffe0b2
style D fill:#c5e1a5
style E fill:#90caf9
style F fill:#ef9a9a
패턴 1️⃣ 의존성 배열 없음 - 매번 실행
useEffect(() => {
console.log("렌더링될 때마다 실행됩니다!");
});
// 의존성 배열을 아예 안 쓰면 렌더링 될 때마다 실행
🚨 주의하세요!
이 패턴은 거의 쓰지 않습니다. 왜냐하면 너무 자주 실행되거든요. 특히 useEffect 안에서 setState를 하면 무한 루프에 빠져요! 제가 초보일 때 이거 모르고 브라우저 먹통 시킨 적 있습니다… ㅋㅋ
패턴 2️⃣ 빈 배열 [] - 마운트 시 1회만
useEffect(() => {
console.log("컴포넌트가 처음 나타날 때 딱 한 번만 실행!");
// 여기서 보통 API 호출해서 데이터 가져옵니다
fetchUserData();
}, []); // 빈 배열 = 처음 한 번만!
이게 가장 많이 쓰는 패턴이에요. 컴포넌트가 처음 화면에 나타날 때 서버에서 데이터를 가져오는 용도로 딱입니다.
패턴 3️⃣ 특정 값 감시 [count] - 값 변경 시
const [count, setCount] = useState(0);
useEffect(() => {
console.log("count가 변경되었습니다:", count);
// count가 바뀔 때마다 이 코드가 실행됩니다
document.title = `클릭 ${count}번`;
}, [count]); // count를 감시!
배열 안에 count를 넣으면, count 값이 바뀔 때마다 useEffect가 실행돼요. 마치 CCTV처럼 특정 값을 감시하는 거죠.
패턴 4️⃣ 클린업 함수 - 언마운트 시 정리
useEffect(() => {
// 타이머 시작
const timer = setInterval(() => {
console.log("1초마다 실행!");
}, 1000);
// return으로 클린업 함수 정의
return () => {
console.log("컴포넌트가 사라질 때 타이머 정리!");
clearInterval(timer); // 타이머 중지
};
}, []);
useEffect 안에서 함수를 return하면, 그게 바로 클린업 함수입니다. 컴포넌트가 사라질 때 실행되면서 뒷정리를 해줘요. 타이머, 이벤트 리스너, 구독 등을 정리할 때 필수입니다!
💡 클린업 함수는 왜 필요한가요?
타이머나 이벤트 리스너를 정리하지 않으면 메모리 누수(Memory Leak)가 발생합니다. 컴포넌트는 사라졌는데 타이머는 계속 돌고 있으면 성능 문제가 생기죠. 마치 불 끄고 나가는 것처럼, 깔끔하게 정리하는 습관이 중요해요!
🚀 실전! Todo 앱으로 배우는 React CRUD
이제 이론은 충분히 배웠으니, 실전으로 가볼까요? 요즘 개발자 면접에서 자주 나오는 Todo 앱을 직접 만들어보겠습니다. CRUD(Create, Read, Update, Delete) 기능을 모두 구현하면서 React의 핵심을 익힐 수 있어요.
프로젝트 구조 잡기
먼저 어떤 컴포넌트가 필요한지 생각해봅시다:
- Header: 오늘 날짜 표시
- TodoEditor: 새로운 할 일 입력
- TodoList: 할 일 목록 + 검색 기능
- TodoItem: 개별 할 일 (체크박스, 삭제 버튼)
데이터 모델링 - 어떻게 저장할까?
할 일 하나하나를 객체로 만들고, 여러 개를 배열에 담아 관리할 거예요.
// 할 일 하나의 구조
{
id: 1, // 고유 식별자 (삭제, 수정할 때 필요)
isDone: false, // 완료 여부
content: "React 공부하기", // 할 일 내용
createdDate: 1701234567 // 생성 시간 (타임스탬프)
}
왜 이렇게 구조를 잡았을까요? id는 나중에 “이 항목을 삭제해!”라고 지정할 때 필요하고, isDone은 체크박스 상태를 관리하기 위해서죠. 날짜는 나중에 정렬할 때 유용해요.
✨ Create - 할 일 추가하기
가장 먼저 구현할 기능은 새로운 할 일을 추가하는 거예요. 여기서 핵심은 불변성을 지키는 겁니다!
// App.js
import { useState, useRef } from 'react';
function App() {
const [todo, setTodo] = useState([]); // 할 일 배열
const idRef = useRef(1); // id 생성용 (렌더링과 무관)
const onCreate = (content) => {
// 새로운 할 일 객체 생성
const newItem = {
id: idRef.current, // 현재 id 사용
content, // ES6 단축 문법
isDone: false,
createdDate: new Date().getTime()
};
// ⭐ 중요: 불변성을 지키며 배열 업데이트
setTodo([newItem, ...todo]); // 맨 앞에 추가
idRef.current += 1; // 다음 id를 위해 증가
};
return (
<TodoEditor onCreate={onCreate} />
);
}
🚨 왜 push() 대신 스프레드 연산자(…)를 쓰나요?
todo.push(newItem)처럼 직접 수정하면 안 됩니다! React는 원본 배열이 바뀌었는지 감지 못해요. 반드시 새로운 배열을 만들어서 setState 해야 리렌더링이 일어납니다. 이게 바로 불변성!
그리고 TodoEditor 컴포넌트에서는 입력값 검증도 해줘야겠죠?
// TodoEditor.js
function TodoEditor({ onCreate }) {
const [content, setContent] = useState("");
const inputRef = useRef(); // 포커스 제어용
const onSubmit = () => {
// 빈 값 체크
if (!content.trim()) {
inputRef.current.focus(); // 포커스 주기
return;
}
onCreate(content); // 부모 함수 호출
setContent(""); // 입력창 비우기
};
// Enter 키 지원
const onKeyDown = (e) => {
if (e.keyCode === 13) onSubmit();
};
return (
<div>
<input
ref={inputRef}
value={content}
onChange={(e) => setContent(e.target.value)}
onKeyDown={onKeyDown}
placeholder="새로운 할 일..."
/>
<button onClick={onSubmit}>추가</button>
</div>
);
}
📖 Read - 리스트 보여주기 & 검색
이제 추가한 할 일들을 화면에 보여줘야겠죠. map() 메서드를 사용해서 배열을 컴포넌트로 변환합니다.
// TodoList.js
function TodoList({ todo, onUpdate, onDelete }) {
const [search, setSearch] = useState("");
// 검색 필터링 함수
const getSearchResult = () => {
return search === ""
? todo // 검색어 없으면 전체
: todo.filter((it) =>
it.content.toLowerCase().includes(search.toLowerCase())
);
};
return (
<div>
{/* 검색바 */}
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="검색어를 입력하세요..."
/>
{/* 리스트 렌더링 */}
{getSearchResult().map((item) => (
<TodoItem
key={item.id} // ⭐ key는 필수!
{...item} // props 펼치기
onUpdate={onUpdate}
onDelete={onDelete}
/>
))}
</div>
);
}
💡 key props는 왜 필요한가요?
React가 리스트의 각 항목을 구별하기 위해서입니다. key가 없으면 경고가 뜨고, 성능도 나빠져요. 항상 고유한 값(보통 id)을 key로 써야 합니다. 절대 index를 key로 쓰면 안 돼요!
검색 기능을 보면 toLowerCase()를 양쪽에 다 써서 대소문자 구분 없이 검색되게 했어요. 사용자 경험을 위한 작은 배려죠!
🔧 Update - 완료 상태 변경
체크박스를 클릭하면 isDone 값을 뒤집어야 해요. 이때도 불변성을 지키면서 업데이트합니다.
// App.js
const onUpdate = (targetId) => {
setTodo(
todo.map((item) =>
// id가 일치하면 isDone을 반대로 변경
item.id === targetId
? { ...item, isDone: !item.isDone } // 객체 복사 후 수정
: item // 일치 안 하면 그대로
)
);
};
여기서 map()을 쓴 이유는? 배열의 모든 항목을 순회하면서 조건에 맞는 것만 수정하기 위해서죠. 삼항 연산자로 깔끔하게 처리할 수 있어요.
🗑️ Delete - 삭제하기
마지막으로 삭제 기능입니다. filter() 메서드로 간단하게 구현할 수 있어요.
// App.js
const onDelete = (targetId) => {
// targetId와 다른 것들만 남기기 = targetId 삭제
setTodo(todo.filter((item) => item.id !== targetId));
};
filter()는 조건을 만족하는 항목만 모아서 새로운 배열을 만듭니다. 삭제하고 싶은 id가 아닌 것들만 걸러내면, 결과적으로 그 항목이 삭제되는 효과가 나는 거죠!
🔍 React Developer Tools로 디버깅하기
코드를 짜다 보면 “어? 왜 안 돼?”하는 순간이 많죠. 이럴 때 Chrome 확장 프로그램인 React Developer Tools가 엄청 유용해요.
설치 및 기본 사용법
- Chrome 웹 스토어에서 “React Developer Tools” 검색 후 설치
- React 앱을 실행하면 개발자 도구에 “Components” 탭이 생깁니다
- 컴포넌트 트리를 보며 Props와 State를 실시간으로 확인 가능!
Highlight Updates - 불필요한 렌더링 찾기
설정에서 “Highlight updates when components render”를 켜면, 리렌더링이 일어날 때마다 컴포넌트가 깜빡입니다. 이걸로 불필요한 렌더링을 찾아서 최적화할 수 있어요.
💡 실전 팁
TodoList에서 하나의 항목만 체크했는데 전체 리스트가 깜빡인다면? 최적화가 필요하다는 신호입니다. 이건 나중에 React.memo나 useMemo로 해결할 수 있어요!
😅 Props Drilling, 이게 문제입니다
여기까지 따라오시느라 수고 많으셨어요! 근데… 뭔가 좀 이상하지 않나요?
graph TD
A[App<br/>onDelete 함수 정의] --> B[TodoList<br/>그냥 전달만]
B --> C[TodoItem<br/>실제 사용]
style A fill:#a8e6cf
style B fill:#ffe0b2
style C fill:#7ec8e3
onDelete 함수를 보면, App에서 만들었는데 실제로 사용하는 건 TodoItem이에요. 그런데 중간에 있는 TodoList는 이 함수를 전혀 안 쓰는데도 받아서 다시 전달만 해야 하죠.
// 이런 식으로 계속 전달...
<TodoList onDelete={onDelete} />
└─ <TodoItem onDelete={onDelete} />
이게 바로 Props Drilling 문제입니다. 컴포넌트 구조가 깊어질수록 데이터 전달이 복잡해지고, 유지보수가 어려워져요. 마치 땅을 파고 내려가는 드릴처럼 props가 계층을 뚫고 내려간다고 해서 붙은 이름이에요.
🚨 Props Drilling의 문제점
- 중간 컴포넌트가 불필요한 props를 받아야 함
- 컴포넌트 구조 변경 시 여러 곳을 수정해야 함
- 코드 가독성이 떨어짐
이 문제는 Context API나 상태 관리 라이브러리(Zustand, Recoil)로 해결할 수 있어요. 다음 단계에서 배우면 됩니다!
🎯 마무리하며
오늘 우리는 React의 핵심을 제대로 배웠습니다:
- ✅ 컴포넌트 생명주기와 useEffect의 4가지 패턴
- ✅ Todo 앱 CRUD 구현을 통한 실전 경험
- ✅ 불변성의 중요성과 올바른 상태 업데이트 방법
- ✅ React Developer Tools로 디버깅하는 방법
- ✅ Props Drilling 문제와 한계점 인식
이제 여러분은 기본적인 React 앱을 만들 수 있는 능력을 갖추었습니다! 하지만 아직 최적화되지 않았고, Props Drilling 같은 문제도 있죠.
다음 단계에서는 useMemo, useCallback으로 성능을 최적화하고, Context API로 Props Drilling을 해결하는 방법을 배워볼 거예요. 더 나아가 TypeScript를 적용하면 타입 안정성까지 확보할 수 있습니다.
오늘 배운 내용으로 직접 Todo 앱을 만들어보세요. 에러가 나도 괜찮아요. 제가 처음 배울 때 얼마나 많은 에러를 만났는지 몰라요. 그 과정이 바로 성장입니다!
질문이나 막히는 부분이 있다면 댓글로 편하게 남겨주세요. 함께 해결해봅시다! 🚀
📚 추천 다음 학습
- React 성능 최적화 (useMemo, useCallback, React.memo)
- Context API로 전역 상태 관리하기
- Custom Hook 만들기
- TypeScript와 React 함께 사용하기
참고 자료: 이 글은 ‘한입 크기로 잘라 먹는 리액트’를 바탕으로 작성되었습니다.