지난주에는 React를 처음 시작하며 컴포넌트, JSX, props를 배웠습니다. 핵심만 빠르게 짚어보겠습니다.
{name}, {1 + 1}, {isAdmin && <Badge />}class는 JS 예약어<>...</>로 감싸면 불필요한 DOM 노드 없이 여러
요소 반환function Card({ title, desc }) {}JSX에서 JS 표현식은 {} 안에 작성합니다. 조건부 렌더링 방법: (1) && 연산자:
조건이 true일 때만 렌더링합니다. {isLoggedIn && <Dashboard />} (2) 삼항
연산자: true/false에 따라 다른 것을 렌더링합니다. {isLoggedIn ? <Dashboard /> : <Login />}
소문자는 HTML 태그, 대문자는 컴포넌트로 구분합니다. JSX가
React.createElement()로 변환될 때 소문자는 문자열('button')로, 대문자는
함수 참조(Button)로 전달됩니다.
React는 단방향 데이터 흐름(부모 → 자식)을 원칙으로 합니다. 자식이 props를 수정하면 데이터 흐름이 깨지고, 어느 컴포넌트가 데이터를 변경했는지 추적하기 어려워집니다. 데이터 변경은 반드시 소유자(부모)에서만 해야 합니다.
일반 JS 변수는 컴포넌트가 리렌더링될 때마다 초기화됩니다. useState는 렌더링 사이에도 값을 유지합니다.
import { useState } from "react";
function Counter() {
// [현재 값, 값을 바꾸는 함수]
const [count, setCount] = useState(0); // 초기값 0
return (
<div>
<p>현재 카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
</div>
);
}
// ❌ 잘못된 방법 — React가 변경을 감지하지 못함
count++;
count = count + 1;
// ✅ 올바른 방법 — setState로만 변경
setCount(count + 1);
React는 setState가 호출될 때 상태 변경을 감지하고 리렌더링합니다. 직접
변경하면 React가 모르기 때문에 화면이 업데이트되지 않습니다.
// ❌ 클로저 함정 — 오래된 count를 참조할 수 있음
setCount(count + 1);
// ✅ 함수형 업데이트 — 항상 최신 상태를 받음
setCount((prev) => prev + 1);
버튼을 빠르게 여러 번 클릭하거나 비동기 처리에서 상태를 업데이트할 때는 함수형 업데이트를 사용해야 안전합니다.
const [user, setUser] = useState({ name: "철수", age: 25 });
// ❌ 직접 수정 — 안 됨
user.age = 26;
// ✅ 스프레드로 새 객체 생성
setUser((prev) => ({ ...prev, age: prev.age + 1 }));
// { name: '철수', age: 26 }
지난 주 클로저 + 불변성 개념이 여기서 만납니다. prev =>
패턴은 클로저를 통해 최신 상태를 받고, 스프레드로 불변 업데이트를 합니다.
사이드 이펙트란 컴포넌트 외부에 영향을 주는 작업입니다 — API 호출, 타이머 설정, DOM 직접 조작 등. useEffect는 이런 작업을 렌더링 이후에 실행합니다.
import { useState, useEffect } from "react";
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// 렌더링 이후에 실행
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => setUser(data));
}, [userId]); // ← 의존성 배열: userId가 바뀔 때마다 재실행
return <div>{user?.name}</div>;
}
// 1. 빈 배열 [] — 마운트 시 한 번만 실행
useEffect(() => {
console.log("컴포넌트가 처음 나타났을 때 한 번만");
}, []);
// 2. 값이 있는 배열 [dep] — dep이 바뀔 때마다 실행
useEffect(() => {
console.log("userId가 바뀔 때마다");
}, [userId]);
// 3. 배열 없음 — 매 렌더링마다 실행 (거의 안 씀)
useEffect(() => {
console.log("렌더링할 때마다");
});
// useEffect 안에서 async 직접 사용 불가
// ❌ 잘못된 방법
useEffect(async () => {
const data = await fetch("/api/data");
});
// ✅ 올바른 방법 — 내부에 async 함수 정의 후 호출
useEffect(() => {
async function loadData() {
try {
const res = await fetch("/api/data");
const data = await res.json();
setData(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
loadData();
}, []);
useEffect(() => {
const timer = setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
// 클린업 함수 — 컴포넌트가 사라지거나 의존성이 바뀌기 전에 실행
return () => clearInterval(timer);
}, []);
타이머, 이벤트 리스너 등을 설정했다면 반드시 클린업 함수에서 제거해야 메모리 누수를 방지할 수 있습니다.
useEffect와 클로저가 만나는 곳에서 버그가 자주 발생합니다. 의존성 배열에 사용하는 값을 빠뜨리면 오래된 클로저(stale closure)를 참조합니다.
// ❌ 버그 — count를 사용하지만 의존성에 없음
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 항상 초기값(0)을 출력
}, 1000);
return () => clearInterval(timer);
}, []); // count를 빠뜨림
// ✅ 해결 — 함수형 업데이트로 의존성 회피
useEffect(() => {
const timer = setInterval(() => {
setCount((prev) => prev + 1); // 함수형 업데이트로 count 의존성 제거
}, 1000);
return () => clearInterval(timer);
}, []);
Q1. useEffect를 사용하여 컴포넌트가 마운트될 때 document.title을 '내
앱'으로 바꾸고, count 상태가 변경될 때마다 '내 앱 (N)'으로 업데이트하는
컴포넌트를 만들어보세요.
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = count === 0 ? "내 앱" : `내 앱 (${count})`;
}, [count]);
return (
<div>
<p>클릭 횟수: {count}</p>
<button onClick={() => setCount((prev) => prev + 1)}>클릭</button>
</div>
);
}
useEffect의 의존성 배열에 count를 넣으면 count가 바뀔 때마다 effect가
재실행됩니다. document.title을 변경하는 것은 DOM 직접 조작이므로
사이드 이펙트에 해당하며, useEffect 안에서 처리하는 것이
올바릅니다.
Q2. 이메일과 메시지를 입력받는 문의하기 폼(ContactForm)을 만들어보세요.
조건: (1) Controlled Component 패턴, (2) 제출 시 e.preventDefault() 호출,
(3) 이메일에 @가 없으면 에러 메시지 표시, (4) 제출 성공 시 입력 필드
초기화
function ContactForm() {
const [email, setEmail] = useState("");
const [message, setMessage] = useState("");
const [error, setError] = useState("");
function handleSubmit(e) {
e.preventDefault();
if (!email.includes("@")) {
setError("올바른 이메일을 입력하세요");
return;
}
setError("");
console.log({ email, message });
setEmail("");
setMessage("");
}
return (
<form onSubmit={handleSubmit}>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="이메일"
/>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="메시지"
/>
{error && <p style={{ color: "red" }}>{error}</p>}
<button type="submit">보내기</button>
</form>
);
}
Controlled Component 패턴으로 각 input의 value를 state로 관리합니다.
onSubmit에서 e.preventDefault()로 새로고침을 방지하고, 유효성 검사 후
상태를 초기화합니다.
Q3. 1초마다 자동으로 숫자가 올라가는 타이머 컴포넌트를 만들어보세요. 조건:
(1) useEffect와 setInterval 사용, (2) 반드시 클린업 함수에서
clearInterval 호출, (3) 함수형 업데이트(prev ={">"} ...) 사용
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return <p>경과 시간: {seconds}초</p>;
}
빈 의존성 배열로 마운트 시 한 번만 타이머를 설정하고, 클린업 함수에서
clearInterval로 정리합니다. setSeconds(prev ={">"} prev + 1) 함수형
업데이트를 사용하여 stale closure 문제를 방지합니다. 클린업
없이 setInterval을 사용하면 컴포넌트가 사라져도 타이머가 계속 실행되어
메모리 누수가 발생합니다.
Q1. useState에서 state를 직접 변경하면 안 되는 이유는?
React는 setState 호출을 감지해서 리렌더링합니다. 직접 변경하면 React가 변경을 모르기 때문에 화면이 업데이트되지 않습니다. 또한 불변성을 유지해야 이전 상태와의 비교가 가능하고 React의 최적화 기능(memo, useMemo 등)이 올바르게 동작합니다.
의존성 배열은 effect가 언제 재실행될지를 제어합니다. 배열이 없으면 매 렌더링마다, 빈 배열이면 마운트 시 한 번만, 값이 있으면 그 값이 변경될 때만 실행됩니다. 의존성을 잘못 명시하면 stale closure 버그가 발생할 수 있습니다.
컴포넌트가 처음 화면에 나타날 때(마운트) 한 번만 실행됩니다. API 초기 데이터 로딩, 이벤트 리스너 등록 등에 주로 사용합니다. 컴포넌트가 화면에서 사라질 때(언마운트) 클린업 함수가 실행됩니다.
1학기를 돌아보면 HTML/CSS로 화면 구조와 스타일링을 배우고, JavaScript로 동작 원리를 이해했으며, 이번 주까지 React의 핵심을 맛봤습니다.
| 구간 | 주제 |
|---|---|
| 1~4주차 | HTML/CSS — 구조, 스타일, 레이아웃 |
| 5~8주차 | JS 기초 — 타입/스코프/클로저/비동기 |
| 9~10주차 | JS 브릿지 — React를 위한 ES6+ 문법 |
| 11~12주차 | React 입문 — 컴포넌트, props, useState, useEffect |
여름 방학 기간에 TypeScript 선택 스터디를 진행합니다. 2학기 React 심화를 위한 준비 과정으로, 타입 시스템과 기본 문법을 다룹니다. 관심 있으신 분은 별도 안내를 확인해주세요.
1학기 동안 함께해 주셔서 감사합니다. 하계 스터디 혹은 2학기 React 심화에서 또 보면 좋겠습니다!