React 기초 (컴포넌트, JSX, 상태 관리)
Vite로 React를 시작하고 JSX·컴포넌트·state·props로 데이터를 받아 상호작용하는 선언형 UI를 만듭니다.
앞 절에서는 Single Page Application (SPA)의 개념과, 왜 현대 웹 개발의 주류가 되었는지 살펴봤습니다.
SPA는 데스크톱 앱에 가까운 매끄러운 경험을 제공하지만, 이를 위해 클라이언트 측에서 UI를 동적으로 갱신하는 로직이 필요합니다.
이 복잡한 UI 개발을 효율화하기 위해 등장한 것이 프론트엔드 프레임워크/라이브러리입니다.
그중 Facebook(현 Meta)에서 만든 React는 UI 라이브러리이며, 선언형(Declarative) 방식으로 UI를 구축/관리하도록 돕습니다.
이번 장에서는 React의 핵심 개념인 컴포넌트(Component), JSX, 상태(State), 속성(Props)을 중심으로 React 애플리케이션을 개발하는 기본적인 방법을 학습합니다.
React 렌더·커밋 생명주기 — 상태 추적
사용자 이벤트→state queue→순수 render→DOM commit→paint의 서로 다른 관찰 시점을 분리하면 setState 직후 DOM이 바뀐다는 오해를 줄인다.
PROCESS · B99 WEB FUNDAMENTALS
React 렌더·커밋 생명주기
이벤트 핸들러가 state update를 예약하면 React가 컴포넌트를 render·reconcile하고, commit 단계에서 필요한 host DOM 변경만 적용한 뒤 브라우저가 paint한다.
렌더는 다음 UI를 계산하고, 커밋은 필요한 DOM 변경을 반영한다. Virtual DOM이 모든 앱의 성능을 자동으로 보장하지는 않는다. useEffect는 상호작용과 일정에 따라 paint 전이나 후에 실행될 수 있다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 이벤트의 snapshot | state: 현재 렌더 값 / DOM: 이전 commit | handler는 현재 props/state를 읽는다 | 사용자 사건이 다음 상태 요청을 만든다 |
| 다음 UI 계산 | state: 다음 렌더 값 / DOM: 아직 이전 commit | 업데이트를 적용해 render 및 결과 비교를 수행한다 | 계산과 실제 DOM 변경은 별개다 |
| 필요한 DOM commit | DOM: 변경 부분 반영 / 화면: 새 표시 | 달라진 부분을 반영한 뒤 브라우저가 표시한다 | render 결과를 commit해야 실제 DOM이 바뀐다 |
React 렌더·커밋 생명주기 — 상태 추적
사용자 이벤트→state queue→순수 render→DOM commit→paint의 서로 다른 관찰 시점을 분리하면 setState 직후 DOM이 바뀐다는 오해를 줄인다.
PROCESS · B99 WEB FUNDAMENTALS
React 렌더·커밋 생명주기
이벤트 핸들러가 state update를 예약하면 React가 컴포넌트를 render·reconcile하고, commit 단계에서 필요한 host DOM 변경만 적용한 뒤 브라우저가 paint한다.
렌더는 다음 UI를 계산하고, 커밋은 필요한 DOM 변경을 반영한다. Virtual DOM이 모든 앱의 성능을 자동으로 보장하지는 않는다. useEffect는 상호작용과 일정에 따라 paint 전이나 후에 실행될 수 있다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 이벤트의 snapshot | state: 현재 렌더 값 / DOM: 이전 commit | handler는 현재 props/state를 읽는다 | 사용자 사건이 다음 상태 요청을 만든다 |
| 다음 UI 계산 | state: 다음 렌더 값 / DOM: 아직 이전 commit | 업데이트를 적용해 render 및 결과 비교를 수행한다 | 계산과 실제 DOM 변경은 별개다 |
| 필요한 DOM commit | DOM: 변경 부분 반영 / 화면: 새 표시 | 달라진 부분을 반영한 뒤 브라우저가 표시한다 | render 결과를 commit해야 실제 DOM이 바뀐다 |
React의 등장 배경과 특징
React는 2013년에 Facebook에 의해 오픈소스로 공개되었습니다.
당시 웹 개발은 jQuery와 같은 라이브러리를 통해 DOM을 직접 조작하는 방식이 주를 이루고 있었는데, 애플리케이션의 규모가 커지고 UI가 복잡해지면서 DOM 조작의 복잡성과 성능 문제가 대두되기 시작했습니다.
React는 이러한 문제에 대한 해결책으로 등장했으며, 다음과 같은 주요 특징을 가집니다.
- 선언형(Declarative): DOM을 직접 조작하는 명령형(Imperative) 방식과 달리, React는 개발자가 이렇게 보이도록 해달라고 선언하면, React가 알아서 그 상태를 만들기 위해 DOM을 효율적으로 업데이트합니다.
- 컴포넌트 기반(Component-Based): UI를 독립적이고 재사용 가능한 작은 조각들인 컴포넌트로 분리하여 개발합니다. 이는 코드의 재사용성을 높이고 유지보수를 용이하게 합니다.
- 가상 DOM (Virtual DOM): React는 UI를 메모리의 구조로 표현하고, 렌더에서 다음 UI를 계산한 뒤 commit 단계에서 필요한 DOM 변경을 반영합니다. 성능은 계산 비용과 렌더 범위 등에 따라 달라지므로, 직접 DOM 접근보다 항상 빠르다고 보장하지는 않습니다.
- 단방향 데이터 흐름 (Unidirectional Data Flow): 데이터가 부모 컴포넌트에서 자식 컴포넌트로 단방향으로 흐릅니다(Props를 통해). 이는 데이터 흐름을 예측 가능하게 하고 디버깅을 용이하게 합니다.
React 개발 환경 설정 (Vite 사용)
가장 간단하게 React 개발 환경을 설정하는 방법은 Vite를 사용하는 것입니다.
Vite는 빠른 개발 서버와 간결한 기본 설정을 제공해 React 애플리케이션 시작에 적합합니다.
이 예제의 Vite 개발 도구를 실행하려면 Node.js를 미리 설치해야 합니다. 만들어진 React UI는 브라우저에서 실행됩니다. Vite 공식 안내의 최소 요구는 Node.js 20.19+ 또는 22.12+이며, 선택한 템플릿이 더 높은 버전을 요구할 수 있으므로 생성 명령의 안내도 확인합니다.
node -v
npm -v
# 또는
yarn -vnpm create vite@latest my-react-app -- --template reactmy-react-app은 생성될 프로젝트의 이름입니다.
cd my-react-app
npm installnpm run dev
# 또는
yarn dev이 명령어를 실행하면 개발 서버가 시작되고, 기본적으로 http://localhost:5173에서 React 애플리케이션을 확인할 수 있습니다.
JSX: JavaScript + XML/HTML
React에서 UI를 정의할 때 사용하는 문법 확장은 JSX(JavaScript XML)입니다.
이는 JavaScript 코드 내부에 HTML과 유사한 마크업을 작성할 수 있도록 해줍니다.
특징- JavaScript 코드 내에서 UI 구조를 선언적으로 표현: 가독성이 높고 UI와 로직을 한 파일에서 관리하기 용이합니다.
- JavaScript 표현식 포함: 중괄호
{}를 사용하여 JSX 내부에서 JavaScript 표현식을 포함할 수 있습니다. - 클래스 대신
className: React의 DOM 속성 이름에 맞춰className을 사용합니다. JSX의 DOM 속성은 대부분onClick처럼 camelCase를 사용하며,aria-*와data-*는 하이픈을 유지합니다. - 자체 닫는 태그: HTML과 달리, 내용이 없는 태그는 반드시
/>로 닫아야 합니다 (예:<img />,<input />). - 최상위 요소: 하나의 컴포넌트가 여러 요소를 렌더링하려면, 반드시 하나의 최상위 요소로 감싸야 합니다 (예:
<div>...</div>,<Fragment>...</Fragment>또는<>...</>).
src/App.jsx)
import React from 'react'; // React 라이브러리 임포트
import './App.css'; // CSS 파일 임포트
function App() {
const name = "리액트";
const isLoggedIn = true;
return (
// JSX는 반드시 하나의 최상위 요소로 감싸져야 함
<div className="App">
<header className="App-header">
<h1>Hello, {name}!</h1> {/* JSX 내부에서 JavaScript 표현식 사용 */}
{isLoggedIn ? ( // 조건부 렌더링
<p>환영합니다!</p>
) : (
<p>로그인 해주세요.</p>
)}
<button onClick={() => alert('버튼 클릭됨!')}>클릭하세요</button> {/* 이벤트 핸들러 */}
</header>
</div>
);
}
export default App; // 컴포넌트 내보내기JSX는 브라우저가 직접 이해할 수 있는 문법이 아닙니다.
Babel과 같은 트랜스파일러를 통해 일반 JavaScript로 변환된 후 브라우저에서 실행됩니다.
(Vite가 이 과정을 자동으로 처리해 줍니다.)
컴포넌트 (Component)
React 애플리케이션은 UI를 독립적이고 재사용 가능한 작은 블록인 컴포넌트(Component)의 조합으로 구성됩니다.
컴포넌트는 마치 레고 블록처럼 조합하여 복잡한 UI를 쉽게 구축할 수 있도록 합니다.
React 컴포넌트는 크게 두 가지 방식으로 작성할 수 있습니다.
- 함수 컴포넌트 (Function Components): 함수 형태로 작성되는 컴포넌트로, React 16.8(Hooks 도입) 이후 주된 방식으로 사용됩니다.
- 클래스 컴포넌트 (Class Components): ES6 클래스 문법으로 작성되는 컴포넌트로,
React.Component를 상속받습니다. 이전 버전 React에서 주로 사용되었습니다. (본 장에서는 함수 컴포넌트 위주로 설명)
src/Greeting.jsx)
import React from 'react';
// Props를 인자로 받음
function Greeting(props) {
return (
<h2>안녕하세요, {props.name}님!</h2>
);
}
export default Greeting;src/App.jsx에서 임포트 후 사용)
import React from 'react';
import Greeting from './Greeting.jsx'; // Greeting 컴포넌트 임포트
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<h1>React 기초</h1>
<Greeting name="개발자" /> {/* Greeting 컴포넌트 사용, name 속성 전달 */}
<Greeting name="리액트 사용자" /> {/* 여러 번 재사용 가능 */}
</header>
</div>
);
}
export default App;위 예시에서 Greeting 컴포넌트는 name이라는 props를 받아 사용자에게 인사말을 표시합니다. 사용자 정의 컴포넌트 이름은 Greeting처럼 대문자로 시작합니다. 소문자 이름은 React가 HTML 태그로 해석합니다.
상태 (State)
상태(State)는 컴포넌트 내부에서 시간이 지남에 따라 변경될 수 있는 데이터를 의미합니다.
사용자의 상호작용(버튼 클릭, 텍스트 입력 등)에 따라 UI가 동적으로 변해야 할 때 state를 사용합니다.
함수 컴포넌트에서는 useState 훅(Hook)을 사용하여 상태를 관리합니다. 훅은 컴포넌트 또는 사용자 정의 훅의 최상위에서 호출합니다. 조건문·반복문 안에서 호출하면 렌더마다 호출 순서가 달라질 수 있습니다.
import React, { useState } from 'react'; // useState 훅 임포트
function Counter() {
// useState 훅 사용: [현재 상태 값, 상태를 업데이트하는 함수] = useState(초기값)
const [count, setCount] = useState(0); // count는 0으로 초기화
const increment = () => {
setCount(count + 1); // setCount 함수를 사용하여 상태 업데이트
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<h3>카운터: {count}</h3>
<button onClick={increment}>증가</button>
<button onClick={decrement}>감소</button>
</div>
);
}
export default Counter;useState 훅의 특징
useState(초기값): 초기 상태 값을 인자로 받습니다.- 반환 값:
[현재 상태 값, 상태를 업데이트하는 함수]형태의 배열을 반환합니다. - 불변성(Immutability):
setCount와 같은 상태 업데이트 함수를 호출할 때, 직접 상태를 변경(count++와 같이)하는 대신 새로운 값을 전달해야 합니다. 새 상태가 이전 상태와 Object.is로 같으면 React는 해당 업데이트의 렌더를 건너뛸 수 있습니다. 부모 렌더나 context 변경도 렌더를 일으킬 수 있습니다.
클릭 한 번을 끝까지 따라가기
초기 화면이 카운터: 0인 상태에서 증가 버튼을 누르면 다음 순서로 진행됩니다.
- 현재 렌더가 만든 핸들러에서
count는0입니다.setCount(count + 1)은 다음 상태를1로 예약합니다. - React는 컴포넌트를 다시 호출해
count가1인 JSX를 계산합니다. 이것이 렌더(render)입니다. - 계산한 결과에 필요한 DOM 변경을 반영하는 커밋(commit)을 거쳐 브라우저가 새 화면을 표시합니다. 컴포넌트를 다시 호출했다고 DOM 전체를 교체하는 것은 아닙니다.
상태 설정 함수는 이미 실행 중인 핸들러의 count를 바꾸지 않습니다. 따라서 다음 코드의 로그는 0이고, 다음 렌더의 화면은 1입니다. React의 렌더·커밋 설명에서 두 단계를 구분합니다.
// count가 0인 렌더에서 만든 이벤트 핸들러 안
setCount(count + 1);
console.log(count); // 0: 이 핸들러가 읽는 상태 스냅샷이전 상태를 바탕으로 여러 번 갱신할 때는 updater 함수를 사용합니다. setCount(previous => previous + 1)은 큐에서 앞선 갱신 결과를 받아 다음 값을 계산합니다. updater 안에서는 로그 전송이나 다른 상태 설정 같은 부수 효과를 실행하지 않습니다. 상태 갱신 큐의 공식 설명을 참고합니다.
속성 (Props)
속성(Props)은 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달할 때 사용됩니다.
props는 "properties"의 줄임말이며, 읽기 전용(Read-only)입니다.
즉, 자식은 전달받은 props를 직접 변경하지 않아야 합니다. 객체 속성을 JavaScript로 바꾸는 것이 가능하더라도 React의 읽기 전용 계약을 어기므로, 부모에게 변경을 요청하는 callback을 사용합니다.
import React from 'react';
function Item(props) {
// props 객체에서 필요한 속성들을 구조 분해 할당으로 가져올 수도 있음
const { name, price } = props;
return (
<li>
{name} - {price}원
</li>
);
}
export default Item;// Item 컴포넌트 사용 예시
import React from 'react';
import Item from './Item.jsx'; // Item 컴포넌트 임포트
import Counter from './Counter'; // Counter 컴포넌트 임포트
import './App.css';
function App() {
const products = [
{ id: 1, name: '노트북', price: 1200000 },
{ id: 2, name: '마우스', price: 30000 },
{ id: 3, name: '키보드', price: 80000 },
];
return (
<div className="App">
<header className="App-header">
<h1>쇼핑 목록</h1>
<ul>
{/* 배열을 map 함수로 순회하여 Item 컴포넌트 렌더링 */}
{products.map(product => (
// key prop은 리스트 렌더링 시 React가 각 아이템을 식별하는 데 사용
// 고유한 값을 가져야 함 (여기서는 product.id)
<Item key={product.id} name={product.name} price={product.price} />
))}
</ul>
<hr />
<Counter /> {/* Counter 컴포넌트 사용 */}
</header>
</div>
);
}
export default App;props는 데이터가 부모에서 자식으로 단방향으로 흐르는 React의 핵심 원칙을 보여줍니다. 위 key는 형제 항목을 식별하기 위한 React의 정보이며 Item에 일반 prop으로 전달되지 않습니다. 항목 ID도 필요하면 id={product.id}를 별도로 전달합니다. 항목이 추가·삭제·재정렬될 때도 같은 항목은 같은 안정적인 key를 가져야 합니다.
컴포넌트, JSX, state, props를 함께 볼 때는 데이터가 어디에서 시작되어 어떤 화면으로 이어지는지 흐름을 먼저 잡는 것이 좋습니다.
TREE
React 상태 소유권과 단방향 흐름
두 자식이 함께 쓰는 상태는 가장 가까운 공통 소유자에 두고, 값은 props로 내려보내며 사용자 의도는 callback과 event로 올려보낸다.
갱신은 SearchPage에서
자식이 callback을 호출하면 소유자의 핸들러가 상태를 갱신하고 새 props를 내려보낸다. 상태의 원본은 이 소유자가 관리한다.
- props: 소유자에서 자식으로
- callback/event: 자식에서 소유자로
상태를 양쪽 자식에 복제하지 않는다. 소유권은 한 곳에 두고 데이터와 의도의 방향을 분리하면 갱신 근거를 추적할 수 있다.
React 기초 정리
React 기초를 정리할 때는 JSX 문법 자체보다 컴포넌트가 어떤 state를 갖고, 어떤 값을 props로 내려보내며, 어떤 이벤트로 다시 갱신되는지 흐름을 잡는 것이 중요합니다.
컴포넌트를 나누고 state, props, JSX를 연결할 때는 상태 소유자, 값의 전달 방향, 이벤트 흐름을 함께 확인해야 합니다.
연습: 상태의 값과 소유자 판단하기
1. count가 4인 렌더의 한 이벤트 핸들러에서 아래 코드를 실행했습니다. 같은 클릭의 두 호출을 마친 뒤 화면에 표시할 값은 얼마일까요? 두 줄을 모두 updater로 바꾸면 어떻게 달라질까요?
setCount(count + 1);
setCount(count + 1);풀이 확인
두 호출 모두 같은 스냅샷 4를 읽으므로 각각 5로 교체하라고 요청합니다. 다음 렌더의 값은 5입니다. 두 줄을 setCount(previous => previous + 1)로 바꾸면 큐에서 4 → 5 → 6으로 계산하여 6이 됩니다. 서로 다른 두 번의 클릭을 이 문제의 한 핸들러와 혼동하지 않습니다.
2. SearchInput에서 입력한 검색어를 ResultList도 표시해야 합니다. 두 자식에 별도 query 상태를 만들면 어떤 문제가 생기며, 상태는 어디에 두어야 할까요?
풀이 확인
별도 원본을 만들면 한쪽만 바뀌어 입력창과 결과 제목이 서로 다를 수 있습니다. 두 자식의 가장 가까운 공통 부모 SearchPage가 query를 소유하고 값을 props로 전달합니다. 입력창은 onQueryChange callback을 호출해 부모의 변경을 요청합니다. 검색어로 계산할 수 있는 필터 결과는 먼저 계산으로 만들 수 있는지 확인합니다.
3. 목록 항목을 클릭할 때 props.key를 읽으니 undefined입니다. 항목 ID 7을 자식에서 받으려면 호출 부분을 어떻게 작성해야 할까요?
풀이 확인
<Item key={7} id={7} name="마우스" price={30000} />처럼 id를 별도로 전달합니다. key는 React가 형제 항목을 식별하는 정보이고, id는 자식이 읽는 일반 prop입니다.