안녕하세요. 11주차입니다. 지난 주에 컴포넌트, JSX, props, useState를 배웠습니다. 이번 주는 그 위에 useEffect, 이벤트 처리, 폼 상태 관리 세 가지를 더합니다. 이 세 가지를 익히면 "API에서 데이터를 가져와서 화면에 보여주고, 사용자 입력을 받아 처리하는" 실제 앱에 가까운 코드를 작성할 수 있게 됩니다.
아래 8문제를 막힘없이 풀 수 있다면,
이번 주 학습 자료를 생략하거나, 모르는 부분만 선택적으로 읽으셔도 좋습니다.
바로 하단의 본문 섹션들을 확인해보세요.
컴포넌트의 주 역할은 화면(JSX)을 반환하는 것입니다. 하지만 실제 앱에서는 화면을 그리는 것 외에도 해야 할 일이 있습니다 — API에서 데이터 가져오기, 타이머 설정, 문서 제목 바꾸기 등. 이런 컴포넌트 외부에 영향을 주는 작업을 사이드 이펙트(side effect)라고 하며, useEffect가 이를 담당합니다.
비유하면, 컴포넌트가 "카페 메뉴판"이라면, useEffect는 "주문이 들어온 뒤 주방에서 음료를 만드는 과정"입니다. 메뉴판(화면)을 보여주는 것과 음료를 만드는 것(사이드 이펙트)은 분리된 작업입니다.
import { useState, useEffect } from "react";
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// 이 함수는 렌더링 이후에 실행됨
fetch(`/api/users/${userId}`)
.then((res) => res.json())
. data
의존성 배열은 "이 effect가 언제 다시 실행될지"를 결정합니다.
// 패턴 1. 빈 배열 [] — 마운트 시 한 번만 실행
useEffect(() => {
console.log("컴포넌트가 처음 나타났을 때 한 번만");
}, []);
// 패턴 2. 값이 있는 배열 [dep] — dep이 바뀔 때마다 실행
useEffect(() => {
console.log("userId가 바뀔 때마다 새 데이터를 가져옴");
}, [userId]);
// 패턴 3. 배열 없음 — 매 렌더링마다 실행 (거의 안 씀)
useEffect(() => {
console.log("렌더링할 때마다 — 주의: 성능 문제 가능");
타이머를 설정했는데 컴포넌트가 화면에서 사라진다면? 타이머는 계속 실행되어 메모리 누수가 발생합니다. 클린업 함수는 이런 상황을 방지합니다. useEffect에서 함수를 return하면 그것이 클린업 함수가 됩니다.
useEffect(() => {
// 설정(setup)
const timer = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
// 클린업(cleanup) — 컴포넌트가 사라지거나 의존성이 바뀔 때 실행
return () => {
clearInterval(timer);
};
}, []);
클린업 함수가 실행되는 타이밍은 두 가지입니다: (1) 컴포넌트가 화면에서 사라질 때(언마운트), (2) 의존성이 변경되어 effect가 재실행되기 직전. 비유하면 카페에서 새 메뉴판으로 교체하기 전에 이전 메뉴판을 먼저 치우는 것과 같습니다.
useEffect의 콜백을 직접 async로 만들면 안 됩니다. async 함수는 Promise를 반환하는데, React는 useEffect의 반환값으로 클린업 함수(또는 undefined)를 기대하기 때문입니다.
// ❌ 잘못된 방법 — useEffect 콜백을 async로 만듦
useEffect(async () => {
const data = await fetch("/api/data");
// React가 반환된 Promise를 클린업 함수로 인식하려고 시도
}, []);
// ✅ 올바른 방법 — 내부에 async 함수를 정의하고 호출
useEffect(() => {
async function fetchData() {
try {
const res = await fetch("/api/data");
const data = await res.json();
setDatadata
function PostList() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function loadPosts() {
try {
const res = await fetch("/api/posts");
res
이 패턴은 React에서 데이터를 가져올 때 가장 기본이 되는 구조입니다. loading, error, data 세 가지 상태를 관리하고, 각 상태에 따라 다른 UI를 보여줍니다.
React에서 이벤트 처리는 HTML과 비슷하지만 몇 가지 차이가 있습니다. camelCase로 이벤트 이름을 쓰고, 함수 참조를 전달합니다.
function ButtonDemo() {
// 이벤트 핸들러 함수 정의
function handleClick() {
alert("버튼이 클릭되었습니다!");
}
return (
<div>
{/* ✅ 함수 참조 전달 — 클릭할 때 실행 */}
<button onClick={handleClick}>클릭</button>
{/* ✅ 인라인 화살표 함수 — 인자가 필요할 때 */}
<button onClick 가장 흔한 실수가 handleClick()처럼 괄호를 붙이는 것입니다. 괄호를 붙이면 컴포넌트가 렌더링되는 시점에 함수가 즉시 실행됩니다. 클릭했을 때 실행하려면 함수 자체를 참조로 전달해야 합니다.
이벤트 핸들러는 자동으로 이벤트 객체를 인자로 받습니다. React는 브라우저 간 차이를 없앤 합성 이벤트(SyntheticEvent)를 제공합니다.
function InputDemo() {
function handleChange(e) {
// e.target — 이벤트가 발생한 DOM 요소
console.log(e.target.value); // 입력된 텍스트
console.log(e.target.name); // input의 name 속성
}
function handleSubmit(e) {
// 폼 제출 시 페이지 새로고침 방지
e.preventDefault();
console.log("폼 제출됨");
HTML에서 input, textarea 등은 자체적으로 값을 관리합니다. 하지만 React에서는 이 값을 state로 관리하여 React가 유일한 데이터 소스가 되도록 합니다. 이것이 Controlled Component(제어 컴포넌트) 패턴입니다.
function NameInput() {
const [name, setName] = useState("");
return (
<div>
{/* value로 표시할 값 지정, onChange로 변경 감지 */}
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="이름을 입력하세요
value만 지정하고 onChange를 빠뜨리면 입력이 안 되는 읽기 전용 input이 됩니다. 반대로 onChange만 있고 value가 없으면 React가 값을 제어하지 못합니다. 반드시 둘 다 함께 사용해야 합니다.
function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
function handleSubmit(e) {
e.preventDefault(); // 페이지 새로고침 방지
// 간단한 유효성 검사
if (!email.includes("@")) {
input이 많아지면 각각 useState를 만드는 것이 번거롭습니다. 객체 하나로 관리하면 코드가 깔끔해집니다. 이때 지난 주 배운 스프레드 연산자가 활약합니다.
function SignupForm() {
const [form, setForm] = useState({
name: "",
email: "",
password: "",
});
// name 속성을 기준으로 해당 필드만 업데이트
function handleChange(e) {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }
핵심은 handleChange 함수 하나로 모든 input을 처리하는 것입니다.
각 input에 name 속성을 주고, e.target.name으로 어떤 필드가 변경되었는지
판단합니다. { ...prev, [name]: value }로 해당 필드만 업데이트하고 나머지는
유지합니다.
지금까지 배운 useState, useEffect, 이벤트 처리, 폼을 모두 사용하는 작은 앱입니다. 각 줄에 어떤 개념이 적용되었는지 주석을 확인해보세요.
import { useState, useEffect } from "react";
function TodoApp() {
const [todos, setTodos] = useState([]); // 할 일 목록 (배열 state)
const [input, setInput] = useState(""); // 입력 필드 (문자열 state)
// 마운트 시 localStorage에서 데이터 불러오기 (useEffect)
useEffect(() => {
const saved = localStorage.getItem("todos");
if (saved) {
saved
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
}, []);
return <p>{count}</p>;
}아래 문제를 직접 코드로 작성해보고, 코드와 함께 댓글로 남겨주세요.
이번 주까지 React의 핵심 기초를 모두 다뤘습니다. 대면 스터디에서는 useState와 useEffect를 더 깊이 다루며 1학기를 마무리합니다. 2학기에는 React Router로 페이지 이동, Context API / Zustand 등 전역 상태 관리, TypeScript 등 React 심화 주제를 다룰 예정입니다.