프론트엔드 스터디 11주차: React 기초 2 —.
useEffect로 사이드 이펙트를 다루고, 이벤트 처리와 폼 상태 관리까지 — React로 실제 동작하는 앱을 만들기 위한 핵심 기술을 배웁니다.
안녕하세요. 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())
.then((data) => setUser(data));
}, [userId]); // ← 의존성 배열: userId가 바뀔 때마다 재실행
if (!user) return <p>로딩 중...</p>;
return <h1>{user.name}</h1>;
}
의존성 배열은 "이 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();
setData(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchData();
}, []);
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");
if (!res.ok) throw new Error("서버 오류");
const data = await res.json();
setPosts(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
loadPosts();
}, []);
if (loading) return <p>로딩 중...</p>;
if (error) return <p>에러: {error}</p>;
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
이 패턴은 React에서 데이터를 가져올 때 가장 기본이 되는 구조입니다. loading, error, data 세 가지 상태를 관리하고, 각 상태에 따라 다른 UI를 보여줍니다.
React에서 이벤트 처리는 HTML과 비슷하지만 몇 가지 차이가 있습니다. camelCase로 이벤트 이름을 쓰고, 함수 참조를 전달합니다.
function ButtonDemo() {
// 이벤트 핸들러 함수 정의
function handleClick() {
alert("버튼이 클릭되었습니다!");
}
return (
<div>
{/* ✅ 함수 참조 전달 — 클릭할 때 실행 */}
<button onClick={handleClick}>클릭</button>
{/* ✅ 인라인 화살표 함수 — 인자가 필요할 때 */}
<button onClick={() => alert("안녕!")}>인사</button>
{/* ❌ 함수 호출 — 렌더링 시 즉시 실행됨! */}
<button onClick={handleClick()}>이건 잘못됨</button>
</div>
);
}
가장 흔한 실수가 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("폼 제출됨");
}
return (
<form onSubmit={handleSubmit}>
<input name="username" onChange={handleChange} />
<button type="submit">제출</button>
</form>
);
}
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="이름을 입력하세요"
/>
<p>입력된 이름: {name}</p>
</div>
);
}
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("@")) {
setError("올바른 이메일 형식이 아닙니다");
return;
}
if (password.length < 6) {
setError("비밀번호는 6자 이상이어야 합니다");
return;
}
setError("");
console.log("로그인 시도:", { email, password });
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="이메일"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="비밀번호"
/>
{error && <p style={{ color: "red" }}>{error}</p>}
<button type="submit">로그인</button>
</form>
);
}
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 }));
// [name]은 계산된 프로퍼티 이름 — name 변수의 값이 키가 됨
}
function handleSubmit(e) {
e.preventDefault();
console.log("가입 정보:", form);
}
return (
<form onSubmit={handleSubmit}>
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
<input
name="password"
type="password"
value={form.password}
onChange={handleChange}
/>
<button type="submit">가입</button>
</form>
);
}
핵심은 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) {
setTodos(JSON.parse(saved));
}
}, []);
// todos가 바뀔 때마다 localStorage에 저장 (useEffect + 의존성)
useEffect(() => {
localStorage.setItem("todos", JSON.stringify(todos));
}, [todos]);
// 폼 제출 이벤트 처리
function handleSubmit(e) {
e.preventDefault(); // 기본 동작 방지
if (!input.trim()) return; // 빈 입력 방지
// 스프레드로 불변 업데이트
setTodos((prev) => [...prev, { id: Date.now(), text: input, done: false }]);
setInput(""); // 입력 필드 초기화
}
// 완료 토글
function toggleTodo(id) {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, done: !todo.done } : todo,
),
);
}
// 삭제
function deleteTodo(id) {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
}
return (
<div>
<h1>할 일 목록</h1>
{/* Controlled Component — value + onChange */}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="할 일을 입력하세요"
/>
<button type="submit">추가</button>
</form>
{/* 리스트 렌더링 — map + key */}
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<span
onClick={() => toggleTodo(todo.id)}
style={{
textDecoration: todo.done ? "line-through" : "none",
cursor: "pointer",
}}
>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>삭제</button>
</li>
))}
</ul>
</div>
);
}
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 심화 주제를 다룰 예정입니다. 이번 학기에서 배운 기초 문법과 대면에서 본 실무 맥락이 그때 다시 연결됩니다.
Post Q&A
프론트엔드 스터디 11주차: React 기초 2 — useEffect, 이벤트 처리, 폼 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!
useEffect로 사이드 이펙트를 다루고, 이벤트 처리와 폼 상태 관리까지 — React로 실제 동작하는 앱을 만들기 위한 핵심 기술을 배웁니다.
지금 당장 읽지 않아도 됩니다. React를 배우면서 에러 처리가 필요해지거나 면접 대비가 필요할 때 참고하세요.
React를 처음 시작합니다. 컴포넌트, JSX, props, useState까지 — React 앱을 만들기 위한 핵심 네 가지를 한 번에 배웁니다.
React를 배우면서 this가 헷갈리거나, 면접 대비가 필요할 때 참고하는 JS 심화 자료입니다. 이 내용을 지금 당장 완벽히 이해하지 않아도 React를 배우는 데 지장이 없습니다.
Next.js가 무엇이고 왜 필요한지 React와 비교해 이해한 뒤, CSR부터 SSR, SSG, ISR, PPR까지 렌더링 전략과 Web Vitals를 정리하며 스터디를 마무리합니다.
다음 주부터 React를 시작합니다. React 코드에서 매 줄 등장하는 map/filter, 구조 분해, 스프레드, 옵셔널 체이닝, 모듈까지 — React 코드를 막힘없이 읽기 위한 JS 문법을 한 번에 정리합니다.
비동기와 async/await를 배운 뒤, 대면에서는 API를 단순 호출 방법이 아니라 프론트엔드와 백엔드 사이의 계약으로 바라봅니다. HTTP, REST, CORS, 프록시, API 클라이언트 구조, TanStack Query와 파일 배치까지 연결합니다.
JS 비동기 처리의 흐름을 배웁니다. 클로저가 왜 중요한지, 콜백 지옥을 해결한 Promise, 그리고 현대 JS의 표준인 async/await까지 이어지는 흐름을 이해합니다.
바닐라 JS의 한계에서 React가 등장한 이유까지, 컴포넌트와 렌더링 방식을 이해하고 프로젝트 구조 감각을 만들어가는 대면입니다.
JS에서 가장 낯선 개념 중 하나인 프로토타입 기반 상속을 다룹니다. const가 왜 불변을 보장하지 않는지, 타입을 런타임에 정확히 확인하는 방법까지 함께 배웁니다.
JavaScript에서 거의 모든 것은 객체입니다. 함수가 일급 객체라는 의미, var와 let의 스코프 차이, 그리고 렉시컬 스코프까지 다룹니다.
4~6주차 공백 이후 진행하는 첫 번째 대면입니다. HTML/CSS/JS 핵심을 가볍게 다시 연결하고, 브라우저가 코드를 실제 화면으로 바꾸는 과정을 통해 렌더링 파이프라인과 성능 감각을 잡습니다.
길어진 6주차 대면의 두 번째 차시입니다. AI 시대에 개발자가 실제로 어떻게 일하는지, 그리고 프론트엔드 개발자가 디자이너·백엔드·PM 사이에서 어떤 연결과 리딩 역할을 하는지 다룹니다.
JavaScript를 처음 제대로 배우는 주차입니다. var/let/const의 차이, JS만의 독특한 타입 시스템, 그리고 호이스팅·TDZ까지 다룹니다.
배경 이미지를 제어하고, Position으로 요소를 원하는 위치에 배치하며, Flexbox로 유연한 가로·세로 레이아웃을 구성합니다. 요소를 화면 정중앙에 띄우거나, 내비게이션 바처럼 가로로 예쁘게 정렬하는 것이 이번 주 목표입니다.
3주차 박스 모델과 스타일링 복습, 과제 풀이, 4주차 Position과 Flexbox 레이아웃까지 다룬 대면 스터디 정리입니다. 면접에서 자주 나오는 마진 겹침, box-sizing, Flexbox 관련 질문도 함께 준비합니다.
폰트와 색상을 다루고, 인라인과 블록 요소의 차이를 이해하며, 마진·패딩으로 구성되는 박스 모델을 완벽하게 익힙니다. 요소들 사이의 여백을 자유자재로 제어하고 화면이 찌그러지지 않게 박스 크기를 다루는 것이 이번 주 목표입니다.
2주차 폼과 CSS 선택자 복습, 과제 풀이, 3주차 박스 모델과 스타일링까지 다룬 대면 스터디 정리입니다. 면접에서 자주 나오는 GET vs POST, CSS 우선순위, 박스 모델 관련 질문도 함께 준비합니다.
사용자 입력을 받는 폼(Form)을 만들고, CSS를 HTML에 연결하는 방법과 원하는 요소를 정확히 선택하는 CSS 선택자 규칙을 배웁니다. 로그인·회원가입 폼을 직접 만들고, CSS로 원하는 요소를 콕 집어내는 것이 이번 주 목표입니다.
1주차 HTML 핵심 복습과 과제 풀이, 2주차 폼(Form)과 CSS 선택자까지 다룬 대면 스터디 정리입니다. 면접에서 자주 나오는 시맨틱 HTML, img alt 속성 관련 질문도 함께 준비합니다.
기초 태그 강의에서 한 걸음 더 나아갑니다. 카카오톡 공유 썸네일, 모바일 데이터 절약, 스크린 리더기를 위한 숨은 정보 등 실무에서 마주하게 될 중급 HTML 개념들을 가볍게 훑어봅니다.
프론트엔드 개발자가 실무에서 하는 일, 2025-2026 기술 스택, 현실적인 연봉과 채용 트렌드를 정리했습니다. 스터디 자료 구조와 앞으로의 방향도 함께 안내합니다.
스터디 커리큘럼 방향을 정하기 위한 세 번째 사전 진단 퀴즈입니다. 자바스크립트의 스코프, 참조 타입의 메모리 할당, DOM 제어와 이벤트 흐름 등 React 실무의 근간이 되는 CS 지식을 점검합니다.
스터디 커리큘럼 방향을 정하기 위한 마지막 사전 진단 퀴즈입니다. React 컴포넌트를 다루기 위해 필수적인 배열 고차 함수(map, filter)와 비동기 통신(async/await) 능력을 점검합니다.
스터디 커리큘럼 방향을 정하기 위한 마지막 사전 진단 퀴즈입니다. 선언적 UI, 상태(State), 단방향 데이터 흐름, 부수 효과(Side Effect) 등 React 생태계 진입을 위한 필수 개념을 점검합니다.
스터디 커리큘럼 방향을 정하기 위한 첫 번째 사전 진단 퀴즈입니다. HTML 태그 구조부터 CSS 박스 모델까지, 실무와 React 환경에서 이 기초들이 왜 중요한지 점검합니다.
스터디 커리큘럼 방향을 정하기 위한 두 번째 사전 진단 퀴즈입니다. 시맨틱 마크업, 폼 이벤트 제어, CSS 우선순위, Flexbox 레이아웃 등 실전 활용 능력을 점검합니다.