LogoSEO Jing
  • All Posts
  • SEO Jing
  • KD Team
  • CLab CoreTeam
  • Study

Contact Me

© 2026 SEOJing. All rights reserved.

프론트엔드스터디ReactuseEffect이벤트폼

프론트엔드 스터디 11주차: React 기초 2 — useEffect, 이벤트 처리, 폼

2026년 6월 8일·29분 읽기

11주차 학습 자료

  • 학습 챕터: 한 입 크기로 잘라먹는 리액트 — 라이프 사이클 / useEffect
  • 자료 바로가기:
    1. useEffect

안녕하세요. 11주차입니다. 지난 주에 컴포넌트, JSX, props, useState를 배웠습니다. 이번 주는 그 위에 useEffect, 이벤트 처리, 폼 상태 관리 세 가지를 더합니다. 이 세 가지를 익히면 "API에서 데이터를 가져와서 화면에 보여주고, 사용자 입력을 받아 처리하는" 실제 앱에 가까운 코드를 작성할 수 있게 됩니다.


스킵 진단 문제

아래 8문제를 막힘없이 풀 수 있다면,

이번 주 학습 자료를 생략하거나, 모르는 부분만 선택적으로 읽으셔도 좋습니다.

바로 하단의 본문 섹션들을 확인해보세요.

Quiz1 / 8
Q.useEffect의 의존성 배열이 빈 배열([])이면 어떻게 동작하나요?

1. useEffect — 렌더링 이후 실행되는 작업

컴포넌트의 주 역할은 화면(JSX)을 반환하는 것입니다. 하지만 실제 앱에서는 화면을 그리는 것 외에도 해야 할 일이 있습니다 — API에서 데이터 가져오기, 타이머 설정, 문서 제목 바꾸기 등. 이런 컴포넌트 외부에 영향을 주는 작업을 사이드 이펙트(side effect)라고 하며, useEffect가 이를 담당합니다.

비유하면, 컴포넌트가 "카페 메뉴판"이라면, useEffect는 "주문이 들어온 뒤 주방에서 음료를 만드는 과정"입니다. 메뉴판(화면)을 보여주는 것과 음료를 만드는 것(사이드 이펙트)은 분리된 작업입니다.

기본 구조

jsx
import { useState, useEffect } from "react";

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // 이 함수는 렌더링 이후에 실행됨
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .  data

의존성 배열 3가지 패턴

의존성 배열은 "이 effect가 언제 다시 실행될지"를 결정합니다.

jsx
// 패턴 1. 빈 배열 [] — 마운트 시 한 번만 실행
useEffect(() => {
  console.log("컴포넌트가 처음 나타났을 때 한 번만");
}, []);

// 패턴 2. 값이 있는 배열 [dep] — dep이 바뀔 때마다 실행
useEffect(() => {
  console.log("userId가 바뀔 때마다 새 데이터를 가져옴");
}, [userId]);

// 패턴 3. 배열 없음 — 매 렌더링마다 실행 (거의 안 씀)
useEffect(() => {
  console.log("렌더링할 때마다 — 주의: 성능 문제 가능");

클린업 함수 — 뒷정리하기

타이머를 설정했는데 컴포넌트가 화면에서 사라진다면? 타이머는 계속 실행되어 메모리 누수가 발생합니다. 클린업 함수는 이런 상황을 방지합니다. useEffect에서 함수를 return하면 그것이 클린업 함수가 됩니다.

jsx
useEffect(() => {
  // 설정(setup)
  const timer = setInterval(() => {
    setSeconds((prev) => prev + 1);
  }, 1000);

  // 클린업(cleanup) — 컴포넌트가 사라지거나 의존성이 바뀔 때 실행
  return () => {
    clearInterval(timer);
  };
}, []);

클린업 함수가 실행되는 타이밍은 두 가지입니다: (1) 컴포넌트가 화면에서 사라질 때(언마운트), (2) 의존성이 변경되어 effect가 재실행되기 직전. 비유하면 카페에서 새 메뉴판으로 교체하기 전에 이전 메뉴판을 먼저 치우는 것과 같습니다.

async/await과 useEffect

useEffect의 콜백을 직접 async로 만들면 안 됩니다. async 함수는 Promise를 반환하는데, React는 useEffect의 반환값으로 클린업 함수(또는 undefined)를 기대하기 때문입니다.

jsx
// ❌ 잘못된 방법 — 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

실전 패턴: 데이터 fetching 컴포넌트

jsx
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를 보여줍니다.


2. 이벤트 처리 — 사용자와 상호작용하기

React에서 이벤트 처리는 HTML과 비슷하지만 몇 가지 차이가 있습니다. camelCase로 이벤트 이름을 쓰고, 함수 참조를 전달합니다.

기본 이벤트 처리

jsx
function ButtonDemo() {
  // 이벤트 핸들러 함수 정의
  function handleClick() {
    alert("버튼이 클릭되었습니다!");
  }

  return (
    <div>
      {/* ✅ 함수 참조 전달 — 클릭할 때 실행 */}
      <button onClick={handleClick}>클릭</button>

      {/* ✅ 인라인 화살표 함수 — 인자가 필요할 때 */}
      <button onClick  

가장 흔한 실수가 handleClick()처럼 괄호를 붙이는 것입니다. 괄호를 붙이면 컴포넌트가 렌더링되는 시점에 함수가 즉시 실행됩니다. 클릭했을 때 실행하려면 함수 자체를 참조로 전달해야 합니다.

이벤트 객체 (e)

이벤트 핸들러는 자동으로 이벤트 객체를 인자로 받습니다. React는 브라우저 간 차이를 없앤 합성 이벤트(SyntheticEvent)를 제공합니다.

jsx
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("폼 제출됨");
  

자주 사용하는 이벤트

  • onClick — 클릭 이벤트 (버튼, 링크 등)
  • onChange — 값 변경 이벤트 (input, select, textarea)
  • onSubmit — 폼 제출 이벤트
  • onFocus / onBlur — 포커스 진입 / 이탈
  • onKeyDown / onKeyUp — 키보드 입력

3. 폼 상태 관리 — Controlled Component 패턴

HTML에서 input, textarea 등은 자체적으로 값을 관리합니다. 하지만 React에서는 이 값을 state로 관리하여 React가 유일한 데이터 소스가 되도록 합니다. 이것이 Controlled Component(제어 컴포넌트) 패턴입니다.

핵심: value + onChange 한 쌍

jsx
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가 값을 제어하지 못합니다. 반드시 둘 다 함께 사용해야 합니다.

실전: 로그인 폼

jsx
function LoginForm() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState("");

  function handleSubmit(e) {
    e.preventDefault(); // 페이지 새로고침 방지

    // 간단한 유효성 검사
    if (!email.includes("@")) {

여러 input을 하나의 state로 관리하기

input이 많아지면 각각 useState를 만드는 것이 번거롭습니다. 객체 하나로 관리하면 코드가 깔끔해집니다. 이때 지난 주 배운 스프레드 연산자가 활약합니다.

jsx
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 }로 해당 필드만 업데이트하고 나머지는 유지합니다.


4. 종합 예제 — 할 일 목록 앱

지금까지 배운 useState, useEffect, 이벤트 처리, 폼을 모두 사용하는 작은 앱입니다. 각 줄에 어떤 개념이 적용되었는지 주석을 확인해보세요.

jsx
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

📝 학습 이해 퀴즈

아래 코드를 읽고 동작을 예측해보세요.
Quiz1 / 2
Q.아래 useEffect에서 문제가 무엇인지 찾고, 어떻게 수정해야 하는지 설명하세요.
js
function Timer() {
const [count, setCount] = useState(0);

useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
}, []);

return <p>{count}</p>;
}

📝 11주차 스터디 인증 미션

아래 문제를 직접 코드로 작성해보고, 코드와 함께 댓글로 남겨주세요.

Quiz1 / 3
Q.useEffect를 사용하여 컴포넌트가 마운트될 때 문서 제목(document.title)을 '내 앱'으로 바꾸고, count 상태가 변경될 때마다 '내 앱 (N)'으로 업데이트하는 컴포넌트를 만들어보세요. 버튼 클릭으로 count를 올리는 기능도 포함해주세요.

다음 주 안내

이번 주까지 React의 핵심 기초를 모두 다뤘습니다. 대면 스터디에서는 useState와 useEffect를 더 깊이 다루며 1학기를 마무리합니다. 2학기에는 React Router로 페이지 이동, Context API / Zustand 등 전역 상태 관리, TypeScript 등 React 심화 주제를 다룰 예정입니다.


관련 포스팅

  • 프론트엔드 스터디 10주차 학습 자료: React 기초 1 — 컴포넌트, JSX, props, useState

포스트 목록

/study/clab-26-1
파일 13개, 폴더 2개
프론트엔드 스터디 1주차: 마크업 그 이상, 실무를 위한 중급 HTML 가이드프론트엔드 스터디 2주차: 사용자와 소통하는 폼 & CSS의 시작프론트엔드 스터디 3주차: 프론트엔드의 첫 번째 벽, 박스 모델과 스타일링프론트엔드 스터디 4주차: 자유자재 레이아웃 (포지션과 플렉스박스)프론트엔드 스터디 5주차: JavaScript 시작 — 타입, 변수, 그리고 JS가 이상한 이유프론트엔드 스터디 6주차: 객체, 함수, 그리고 스코프프론트엔드 스터디 7주차: 불변성, 프로토타입, 타입 체크프론트엔드 스터디 8주차: 클로저, Promise, async/await프론트엔드 스터디 9주차: React 입문 전 필수 JS 문법 — map, 구조 분해, 스프레드프론트엔드 스터디 10주차: React 기초 1 — 컴포넌트, JSX, props, useState프론트엔드 스터디 11주차: React 기초 2 — useEffect, 이벤트 처리, 폼프론트엔드 스터디 심화: this, 실행 컨텍스트, 이터러블프론트엔드 스터디 심화: 에러 처리와 정규 표현식
then
(
(
data
)
=>
setUser
(
)
)
;
}, [userId]); // ← 의존성 배열: userId가 바뀔 때마다 재실행
if (!user) return <p>로딩 중...</p>;
return <h1>{user.name}</h1>;
}
}
)
;
(
)
;
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchData();
}, []);
if
(
!
.
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>
);
}
=
{
(
)
=>
alert
(
"안녕!"
)
}
>
인사
</button>
{/* ❌ 함수 호출 — 렌더링 시 즉시 실행됨! */}
<button onClick={handleClick()}>이건 잘못됨</button>
</div>
);
}
}
return (
<form onSubmit={handleSubmit}>
<input name="username" onChange={handleChange} />
<button type="submit">제출</button>
</form>
);
}
"
/>
<p>입력된 이름: {name}</p>
</div>
);
}
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>
);
}
)
)
;
// [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>
);
}
setTodos
(
JSON
.
parse
(
)
)
;
}
}, []);
// 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>
);
}