안녕하세요. 10주차입니다. 드디어 React를 시작합니다. 지난 주까지 배운 map, 구조 분해, 스프레드, 모듈 같은 ES6+ 문법이 이번 주부터 실제 React 코드에서 어떻게 쓰이는지 직접 확인하게 됩니다. 이번 주는 React의 가장 기본이 되는 네 가지 — 컴포넌트, JSX, props, useState를 다룹니다. 이 네 가지만 이해하면 간단한 인터랙티브 앱을 만들 수 있습니다.
아래 8문제를 막힘없이 풀 수 있다면,
이번 주 학습 자료를 생략하거나, 모르는 부분만 선택적으로 읽으셔도 좋습니다.
바로 하단의 본문 섹션들을 확인해보세요.
React는 Facebook(Meta)이 만든 UI 라이브러리입니다. 웹 페이지의 화면을 작은 조각(컴포넌트)으로 나누어 조립하는 방식으로 개발합니다. 레고 블록처럼 작은 부품을 만들어 놓고, 조합해서 복잡한 화면을 완성하는 것이 핵심 아이디어입니다.
1. 선언적(Declarative) — "어떻게"가 아니라 "무엇을" 보여줄지만 작성합니다.
// 명령형 — DOM을 직접 조작 (jQuery 스타일)
const button = document.createElement("button");
button.textContent = "클릭";
button.addEventListener("click", () => {
button.textContent = "클릭됨!";
});
document.body.appendChild(button);
// 선언형 — React 스타일: "이런 화면을 보여줘"
function App() {
const [clicked, setClicked] = useState(false);
명령형은 "버튼을 만들고, 텍스트를 넣고, 이벤트를 달고..."처럼 순서대로 지시합니다. 선언형은 "상태가 이러면 화면은 이렇게 보여줘"라고 결과만 선언합니다. 카페에서 "아메리카노 한 잔 주세요"라고 말하는 것과 같습니다. 원두를 갈고 물을 끓이는 과정은 알아서 처리됩니다.
2. 컴포넌트 기반(Component-Based) — UI를 독립적인 조각으로 나눕니다.
// Header, Sidebar, Content 각각이 독립적인 컴포넌트
function App() {
return (
<div>
<Header />
<Sidebar />
<Content />
</div>
);
}
3. 단방향 데이터 흐름(One-way Data Flow) — 데이터는 항상 부모에서 자식으로 흐릅니다. 자식이 부모의 데이터를 직접 바꿀 수 없습니다. 이 덕분에 데이터가 어디서 변경되었는지 추적하기 쉽습니다.
React에서 컴포넌트는 UI 조각을 반환하는 JavaScript 함수 입니다. 버튼 하나, 카드 하나, 페이지 전체 모두 컴포넌트가 될 수 있습니다.
// 가장 간단한 컴포넌트
function Greeting() {
return <h1>안녕하세요!</h1>;
}
// 다른 컴포넌트 안에서 사용
function App() {
return (
<div>
<Greeting />
<Greeting />
</div>
);
}
React(정확히는 JSX)는 대문자로 시작하면 컴포넌트, 소문자로 시작하면 HTML 태그로 구분합니다. 이 규칙을 어기면 컴포넌트가 렌더링되지 않습니다.
// ✅ 대문자 — React 컴포넌트로 인식
<MyButton /> → React.createElement(MyButton)
// ❌ 소문자 — HTML 태그로 인식 (존재하지 않는 태그)
<myButton /> → React.createElement('mybutton')
JSX는 JavaScript 안에서 HTML과 비슷한 문법으로 UI를 작성할 수 있게 해주는 문법 확장입니다. 브라우저가 직접 이해하는 것이 아니라, 빌드 과정에서 JavaScript 함수 호출로 변환됩니다.
// ❌ 에러 — 루트 요소가 두 개
function App() {
return (
<h1>제목</h1>
<p>내용</p>
);
}
// ✅ Fragment로 감싸기 — 불필요한 DOM 노드 없음
function App() {
return (
<>
<h1>제목</h1>
<p>내용function Profile({ user }) {
return (
<div>
{/* 변수 */}
<h1>{user.name}</h1>
{/* 계산 */}
<p>나이: {user.age + 1}살 (내년 기준)</p>
{/* 함수 호출 */}규칙 3. class 대신 className — class는 JavaScript
예약어이기 때문입니다.
// ❌ HTML 습관대로 쓰면 안 됨
<div class="container">
// ✅ JSX에서는 className
<div className="container">
규칙 4. 조건부 렌더링 — if문은 표현식이 아니므로 안에 쓸 수 없습니다. 대신 && 연산자와 삼항 연산자를 사용합니다.
function Dashboard({ isLoggedIn, notifications }) {
return (
<div>
{/* && — 조건이 true일 때만 렌더링 */}
{isLoggedIn && <p>환영합니다!</p>}
{/* 삼항 — true/false에 따라 다른 것을 렌더링 */}
{isLoggedIn ? <LogoutButton /> : <LoginButton />}
{/* 숫자 0 주의 — 0은 falsy이지만 화면에 0이 출력됨 */props(properties)는 부모 컴포넌트가 자식 컴포넌트에게 데이터를 전달하는 방법입니다. HTML 속성을 쓰듯이 전달하고, 자식은 함수의 매개변수처럼 받습니다.
// 부모에서 props 전달
function App() {
return <UserCard name="철수" age={25} isAdmin={true} />;
}
// 자식에서 구조 분해로 수신 — 지난 주 배운 문법!
function UserCard({ name, age, isAdmin }) {
return (
<div>
<h2>{name}</h2props는 부모가 준 데이터이므로, 자식에서 절대 수정하면 안 됩니다. 물건을 빌려 쓰는 것과 같습니다 — 빌린 물건을 마음대로 바꾸면 안 되겠죠. 데이터를 바꿔야 한다면 부모에서 변경해야 합니다.
// ❌ props 직접 수정 — 절대 하면 안 됨
function UserCard({ name }) {
name = "영희"; // React의 단방향 원칙 위반
return <h2>{name}</h2>;
}
배열 데이터를 화면에 렌더링할 때 map()을 사용합니다. 이때 각 요소에 key prop을 반드시 전달해야 합니다. key는 React가 어떤 요소가 변경/추가/삭제되었는지 효율적으로 판단하는 데 사용하는 고유 식별자입니다.
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
// key는 고유한 id를 사용 — 배열 인덱스는 피하세요
<li key={todo.id}>
{todo.text} {todo.done && "✓"}
</li>
))}
key에 배열 인덱스를 쓰면 요소가 추가/삭제/재정렬될 때 React가 헷갈립니다. 데이터의 고유한 id를 사용하는 것이 올바른 방법입니다.
일반 JavaScript 변수는 컴포넌트가 리렌더링될 때마다 초기화됩니다. 버튼을 클릭해서 숫자를 1 올려도, 리렌더링 되면 다시 0으로 돌아갑니다. useState는 렌더링 사이에도 값을 유지하는 React의 방법입니다.
import { useState } from "react";
function Counter() {
// [현재 값, 값을 바꾸는 함수] = useState(초기값)
const [count, setCount] = useState(0);
return (
<div>
<p>현재 카운트: {count}</p>
<button onClick={() => setCount(count + 1)}useState는 배열을 반환합니다. 지난 주 배운 배열 구조 분해로 첫 번째 요소(현재 값)와 두 번째 요소(변경 함수)를 받습니다. 변수 이름은 자유롭게 지정할 수 있지만, [something, setSomething] 패턴이 관례입니다.
// ❌ 직접 변경 — React가 변경을 감지하지 못함
count = count + 1;
// ✅ setState — React가 감지하고 리렌더링 실행
setCount(count + 1);
React는 setState가 호출될 때 상태가 바뀌었다는 것을 인식하고, 컴포넌트를 다시 실행(리렌더링)합니다. 변수를 직접 바꾸면 React가 모르기 때문에 화면이 업데이트되지 않습니다.
상태를 이전 값 기준으로 변경할 때는 함수형 업데이트 패턴을 사용하는 것이 안전합니다.
// ❌ 위험 — 빠른 연속 클릭 시 오래된 count를 참조할 수 있음
setCount(count + 1);
// ✅ 안전 — 항상 최신 상태(prev)를 받아서 계산
setCount((prev) => prev + 1);
왜 이런 차이가 생길까요? React는 setState 호출을 모아서 한 번에 처리(배칭)할
수 있습니다. 이때 count는 현재 렌더링 시점의 값이므로 여러 번 호출해도 같은
결과가 나올 수 있습니다. 하지만 함수형 업데이트는 항상 직전 상태를 인자로
받으므로 안전합니다.
// 버튼 한 번 클릭에 +3을 하고 싶은 경우
function handleClick() {
// ❌ 세 번 호출해도 +1만 됨 (모두 같은 count를 참조)
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// ✅ 세 번 호출하면 +3이 됨 (각각 최신 prev를 받음)
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
}
하나의 컴포넌트에서 여러 상태가 필요하면 useState를 여러 번 호출합니다.
function SignupForm() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [agreed, setAgreed] = useState(false);
return (
<form>
<input value={name} onChange={(e) => setName(e 아래 코드를 읽고 결과를 예측해보세요. 가능하면 직접 실행하지 말고 먼저 머릿속으로 생각해본 뒤 확인하세요.
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>+3?</button>
</div>
);
}아래 문제를 직접 코드로 작성해보고, 코드와 함께 댓글로 남겨주세요.
다음 주에는 useEffect, 이벤트 처리, 폼 상태 관리를 다룹니다. 이번 주에 배운 useState와 함께 사용하면 API에서 데이터를 가져오거나, 사용자 입력을 처리하는 실제 앱에 가까운 코드를 작성할 수 있게 됩니다.