안녕하세요. 11주차입니다. 지난 주에 컴포넌트, JSX, props, useState를 배웠습니다. 이번 주는 그 위에 useEffect, 이벤트 처리, 폼 상태 관리 세 가지를 더합니다. 이 세 가지를 익히면 "API에서 데이터를 가져와서 화면에 보여주고, 사용자 입력을 받아 처리하는" 실제 앱에 가까운 코드를 작성할 수 있게 됩니다.
이번 주 대면에서는 배포와 운영을 다룹니다. 비대면에서 작성하는 React 코드는 localhost에서만 도는 예제가 아니라, 결국 빌드되고 배포되어 실제 사용자가 만지는 화면이 됩니다. 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의 핵심 기초를 모두 다뤘습니다. 비대면 자료에서는 컴포넌트, props, useState, useEffect, 이벤트, 폼까지 실제 앱을 만들기 위한 최소 단위를 다뤘고, 대면 스터디에서는 이 코드가 실제 서비스가 되기 위해 필요한 배포와 운영을 다룹니다.
2학기에는 React Router로 페이지 이동, Context API / Zustand 등 전역 상태 관리, TypeScript, 데이터 fetching 라이브러리 등 React 심화 주제를 다룰 예정입니다. 이번 학기에서 배운 기초 문법과 대면에서 본 실무 맥락이 그때 다시 연결됩니다.