다크 모드
Chapter 00. 시작하기 전에: jQuery 개발자를 위한 React 19.3 준비운동
"jQuery로 수년간 화면을 만들어 왔지만 React는 한 번도 써 본 적이 없다. 그런데 1장 코드를 펼치자마자
import,=>,async,interface,useState<Post[]>가 쏟아진다. 이 장은 그 낯선 단어들을 미리 한 번씩 만나 두는 준비운동이다."
📋 이 장에서 처음 만나는 개념
| 개념 | 한 줄 요약 | 설명 위치 |
|---|---|---|
| npm, Vite, HMR | 라이브러리 관리 도구 / 초고속 개발 서버 겸 번들러 / 실시간 화면 갱신 | 0.2 |
SPA, JSX (.tsx) | 단일 HTML 기반 웹 앱 / 자바스크립트 함수 호출로 변환되는 UI 문법 | 0.2 |
| 불변성 (Immutability) | 기존 객체·배열을 직접 수정하지 않고 새 참조(...spread, filter, map)를 만드는 규칙 | 0.3 |
| ES6+ 필수 문법 | const/let, 화살표 함수, 구조 분해 할당, async/await | 0.3 |
| TypeScript 최소 문법 | interface, 제네릭 <T>, 유니온 타입 |, 타입 단언 as | 0.4 |
axios와 res.data | Promise 기반 HTTP 클라이언트와 응답 본문 접근법 | 0.5 |
| React 19.3 로드맵 | useState, useActionState, useEffectEvent, <Activity>, React Compiler | 0.6 |
TIP
🏁 이 장의 시작 파일
이 장은 본격적인 코딩에 앞서 개발 도구와 문법 지도를 익히는 준비운동 장입니다. 아직 실습 파일을 수정하지 않으며, 1장부터 frontend/src/BoardApp.tsx를 직접 작성합니다.
0.1 이 책을 읽는 방법
이 책은 jQuery나 JSP는 익숙하지만 React 19.3, 최신 자바스크립트(ES6+), TypeScript는 처음인 개발자를 기준으로 썼습니다. 처음 보는 문법이 나왔을 때 당황하지 않도록 다음 네 가지 장치를 둡니다.
| 장치 | 모양 | 의미 |
|---|---|---|
| 장 첫머리 "처음 만나는 개념" 표 | 📋 표 | 그 장에서 처음 등장하는 문법·도구 목록과 어디서 설명하는지(바로 아래 / 0장 / 뒤 장) |
| 🆕 처음 보는 문법 박스 | ::: info 박스 | 코드에 처음 등장한 문법과 TypeScript 표기를 그 자리에서 한 줄씩 풀이 |
| 🔬 심화 토글 박스 | ::: details 박스 | 초보자는 건너뛰어도 실습에 지장 없지만, 중급자·실무자가 알아야 할 내부 원리와 엣지 케이스 |
| 💪 직접 해보기 실습 미션 | 장 말미 미션 + 정답 토글 | 배운 코드를 응용해 직접 기능을 추가해 보고 정답 코드와 맞춰 보는 훈련 |
이 장(0장)은 끝까지 한 번 가볍게 훑어보고, 1장부터 실습하다가 모르는 문법이 나오면 다시 돌아와 찾아보는 사전처럼 쓰십시오.
0.2 개발 도구 지도: <script src>에서 npm과 Vite로
jQuery 시절에는 HTML에 <script src="jquery.min.js">를 넣으면 끝이었습니다. 모던 프론트엔드는 다음 도구들이 그 역할을 나눠 맡습니다.
| 도구 / 용어 | 한 줄 설명 | jQuery 시절에 비유하면 |
|---|---|---|
| Node.js | 브라우저 밖(PC, 서버)에서 자바스크립트를 실행하는 런타임 | – (개발 도구들을 돌리는 엔진) |
| npm | 라이브러리 설치·관리 도구. npm install로 package.json에 적힌 라이브러리를 node_modules/ 폴더에 내려받음 | jQuery 파일을 직접 다운로드해 js/lib/에 넣던 일 |
package.json | 프로젝트가 쓰는 라이브러리 목록과 버전, 실행 명령어(scripts)를 적은 설정 파일 | Maven의 pom.xml, Gradle의 build.gradle |
npm run dev | package.json의 scripts.dev에 적힌 명령(vite)을 실행 | 로컬 톰캣(Tomcat) 서버 띄우기 |
| npx | 설치된 도구를 바로 실행. 예: npx tsx 파일.ts, npx vitest | – |
| Vite | 초고속 개발 서버 + 번들러. .tsx/.ts 파일을 브라우저가 이해하는 순수 JS로 실시간 변환해 줌 | – (jQuery는 변환이 필요 없었음) |
| HMR (Hot Module Replacement) | 파일을 저장하면 새로고침 없이 바뀐 컴포넌트만 화면에 즉시 반영 | 코드 고치고 F5를 연타하던 일 |
빌드(npm run build) | 배포용으로 타입 검사(tsc) 후 코드를 압축·묶어 dist/ 폴더에 HTML/CSS/JS 정적 파일로 생성 | 압축된 .min.js를 만들던 일 |
실습 전에 설치할 개발 도구 2가지 (꼭 설치하십시오)
| 도구 | 설치 위치 | 왜 필요한가 |
|---|---|---|
| ESLint & Prettier | VS Code 확장(Extensions) 마켓플레이스 | .tsx 파일의 문법 오류, React 훅 규칙 위반(예: 조건문 안의 useState 호출), 코드 포맷팅을 실시간 빨간 줄로 잡아 줍니다. |
| React Developer Tools | Chrome / Edge 확장 프로그램 스토어 | 브라우저 개발자 도구(F12)에 Components와 Profiler 탭이 생깁니다. 컴포넌트 트리, 각 컴포넌트의 props와 useState/useRef 값이 실시간으로 바뀌는 모습, 리렌더링 원인을 눈으로 볼 수 있습니다. |
TIP
1장에서 게시판이 뜨면 F12 → Components 탭 → BoardApp을 선택해 보십시오. 글을 등록하거나 삭제할 때마다 hooks 안의 State (posts) 배열이 바로 바뀌는 것이 보입니다. "상태가 바뀌면 화면이 따라온다"(0.7)를 가장 빨리 체감하는 방법입니다.
SPA(Single Page Application)
jQuery 게시판은 보통 페이지마다 HTML(또는 JSP)이 따로 있고, 이동할 때마다 서버가 새 HTML을 내려줍니다. SPA는 index.html 한 장만 내려받은 뒤, 화면 전환은 자바스크립트가 DOM을 바꿔서 처리합니다. 그래서 페이지 이동이 빠르지만, 메모리 관리(13장)와 URL 관리(4.5장, 11장)를 프론트엔드가 책임져야 합니다.
🍽️ 비유: 뷔페 접시(SPA) vs 식당 문 밖으로 쫓겨나기(전통 웹)
과거 JSP/jQuery 방식은 김치찌개 먹다가 밥 한 공기 추가 주문(link.html)했을 때, 식당 문 밖으로 쫓겨났다가 다시 들어와서 숟가락부터 식판까지 처음부터 다시 세팅받는 꼴이었습니다(화면 전체 깜빡임 + 모든 자바스크립트 변수 증발!). 반면 SPA는 뷔페 접시(index.html) 하나를 들고 다니면서, 빈 반찬 칸만 젓가락으로 쏙쏙 바꿔 담는 것과 같습니다. 접시는 그대로 있고 음식(DOM)만 바뀝니다.
JSX와 .tsx 컴포넌트 파일: HTML이 아니라 자바스크립트 객체다!
React 컴포넌트는 .tsx(TypeScript + JSX) 파일에 HTML처럼 생긴 문법을 작성합니다. 이를 **JSX(JavaScript XML)**라고 부릅니다.
tsx
// BoardHeader.tsx
export function BoardHeader() {
const badgeCount = 5;
return (
<div className="card-header">
<h2>사내 게시판</h2>
<span>알림: {badgeCount}건</span>
</div>
);
}많은 초보자가 JSX를 "자바스크립트 안에 HTML 문자열을 쓰는 문법"으로 오해하지만, JSX는 빌드 시점에 Vite가 순수한 자바스크립트 함수 호출(_jsx)로 변환합니다:
javascript
// Vite 번들러가 변환한 실제 코드 (React 19.3 jsx-runtime)
import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime';
export function BoardHeader() {
const badgeCount = 5;
return _jsxs('div', {
className: 'card-header',
children: [
_jsx('h2', { children: '사내 게시판' }),
_jsxs('span', { children: ['알림: ', badgeCount, '건'] })
]
});
}이 사실을 기억하면 React JSX의 필수 규칙 4가지가 단숨에 이해됩니다:
- 반드시 닫는 태그가 있어야 함:
<input />,<img />,<br />(함수 호출의 인자 객체이므로 문법이 닫혀야 함) - 최상위 반환 요소는 단 하나여야 함: 자바스크립트 함수는 한 번에 두 개의 값을
return할 수 없기 때문입니다. 불필요한<div>없이 여러 요소를 묶고 싶다면 빈 태그<> ... </>(Fragment)로 감쌉니다. class대신className,for대신htmlFor사용: 자바스크립트 예약어와 겹치지 않도록 DOM 프로퍼티 표준 이름을 씁니다.- 중괄호
{ }한 겹으로 자바스크립트 표현식 삽입: HTML 텍스트나 속성 자리에 변수·계산식을 넣을 때는{badgeCount}처럼 중괄호 한 겹을 씁니다. 단,style={{ color: 'red', padding: '10px' }}처럼 인라인 스타일을 줄 때는 바깥{ }는 "자바스크립트 표현식을 넣겠다"는 뜻이고 안쪽{ }는 "스타일 객체 리터럴"이라는 뜻입니다.
CORS와 개발 서버 프록시
브라우저는 보안상 출처(Origin: 프로토콜+도메인+포트)가 다른 서버로 보낸 AJAX 요청의 응답을 자바스크립트가 읽지 못하게 막습니다(CORS 정책). 이 책의 프론트엔드는 localhost:3000, 백엔드는 localhost:8080이라 포트가 다릅니다.
그래서 Vite 개발 서버가 프록시(대리인) 역할을 합니다. 브라우저는 같은 출처인 localhost:3000/api/posts로 요청하고, Vite가 이를 몰래 localhost:8080/api/posts로 전달해 줍니다. 설정은 frontend/vite.config.ts의 server.proxy에 이미 들어 있습니다. 운영 환경에서는 Nginx가 같은 역할을 합니다(부록 A).
0.3 jQuery 시절 자바스크립트(ES5) → 모던 자바스크립트(ES6+)와 불변성
jQuery 코드는 보통 var와 function, 그리고 원본 객체를 직접 고치는 가변(Mutation) 방식으로 작성됩니다. 이 책의 코드는 ES6+ 문법과 불변성(Immutability) 원칙을 따릅니다.
1) 왜 React는 기존 객체·배열을 직접 고치면 안 되는가? (불변성)
React를 시작하는 jQuery 개발자가 가장 먼저 부딪히는 벽이 바로 불변성입니다.
typescript
// ❌ React에서 절대 하면 안 되는 코드 (가변 Mutation)
const [post, setPost] = useState({ title: '공지', viewCount: 0 });
post.viewCount++; // 원본 객체의 메모리 주소(참조)는 그대로임!
setPost(post); // React: "어? 이전 post 주소랑 똑같네(`Object.is`)? 바뀐 게 없구나!" -> 리렌더링 스킵!React는 성능을 위해 객체 내부의 수백 개 속성을 일일이 깊은 비교(Deep Compare)하지 않고, 오직 **메모리 주소(참조, Reference)가 바뀌었는지만 Object.is로 0.001초 만에 비교(Shallow Compare)**합니다. 따라서 상태가 바뀌었음을 React에게 알리려면 반드시 새로운 객체·배열(새로운 메모리 주소)을 만들어 전달해야 합니다.
typescript
// ✅ 올바른 불변성 유지 방식 (새 객체 생성)
setPost({
...post,
viewCount: post.viewCount + 1
});📄 비유: 화이트보드 칠판(가변) vs 인쇄된 새 결재서류(불변)
- jQuery (화이트보드): 칠판에 적힌 글씨를 지우개로 슥슥 지우고 새 숫자를 씁니다. 편하지만 누가 언제 숫자를 바꿨는지 과거 추적이 안 됩니다.
- React (인쇄된 새 결재서류): 기존 문서를 수정테이프로 덮어쓰지 않고, 바뀐 내용이 적힌 완전히 새로운 결재서류를 매번 인쇄해서 결재판에 올립니다. React(결재자)는 서류의 문서 번호(메모리 주소)만 슥 보고 "새 서류가 올라왔군!" 하고 즉시 화면에 공포(리렌더링)합니다.
2) 변수 선언: var → const / let
typescript
var count = 0; // ES5: 함수 단위 스코프, 재선언 가능 → 실수가 잦음
const MAX = 10; // ES6: 다시 대입할 수 없는 상수 (이 책 대부분의 변수)
let page = 1; // ES6: 다시 대입할 수 있는 변수 (블록 { } 단위 스코프)
page = 2; // OK
// MAX = 20; // ❌ TypeError: Assignment to constant variable.3) 화살표 함수: function () {} → () => {}
typescript
$('#btn').on('click', function () { loadPosts(); }); // ES5
useEffect(() => { loadPosts(); }, []); // ES6 화살표 함수
const double = (n: number) => n * 2; // 본문이 한 줄이면 { }와 return 생략 가능
posts.filter(p => p.id !== id); // 매개변수가 하나면 ( )도 생략 가능4) 템플릿 리터럴: 문자열 + 결합 → 백틱(`)과 ${ }
typescript
var url = '/api/posts/' + id + '/like'; // ES5
const url = `/api/posts/${id}/like`; // ES6: 백틱 안에서 ${ }로 값 삽입5) 구조 분해 할당(Destructuring)과 전개(Spread) 연산자
typescript
const post = { id: 1, title: '공지', author: '김개발' };
// 객체 구조 분해: 속성을 꺼내 같은 이름의 변수로 만든다 (Props 받을 때 매일 사용)
const { title, author } = post;
// 배열 구조 분해: 순서대로 꺼내 원하는 이름의 변수로 만든다 (useState에서 매일 사용)
const [posts, setPosts] = useState<Post[]>([]);
// 전개 연산자(...): 객체/배열의 내용을 펼쳐서 얕은 복사 (불변성 유지의 핵심!)
const nextPost = { ...post, title: '수정된 공지' }; // 새 객체 생성
const nextPosts = [newPost, ...posts]; // 맨 앞에 추가한 새 배열 생성6) 배열 불변 메서드: push / splice 금지 → filter / map / find
| 하고 싶은 일 | ❌ 원본 파괴 (React 금지) | ✅ 새 배열 반환 (React 표준) |
|---|---|---|
| 맨 앞에 추가 | posts.unshift(newPost) | [newPost, ...posts] |
| 맨 뒤에 추가 | posts.push(newPost) | [...posts, newPost] |
| 특정 항목 삭제 | posts.splice(idx, 1) | posts.filter(p => p.id !== id) |
| 특정 항목 수정 | posts[idx].title = '새 제목' | posts.map(p => p.id === id ? { ...p, title: '새 제목' } : p) |
| 화면 리스트 변환 | $.each(posts, ...) | posts.map(p => <tr key={p.id}>...</tr>) |
7) 모듈: 전역 변수 → import / export
typescript
// src/hooks/usePagination.ts
export function usePagination() { /* ... */ } // 이름 있는 내보내기 (Named Export)
// src/BoardApp.tsx
import { useState, useEffect } from 'react'; // 라이브러리에서 필요한 훅만 가져오기
import axios from 'axios'; // 기본 내보내기(Default Export) 가져오기
import { usePagination } from '@/hooks/usePagination'; // @ = src 폴더 (5장 참고)
import type { Post } from '@/types/post'; // type: 타입만 가져오기 (빌드 시 완전 제거됨)8) 비동기: 콜백(success:) → Promise → async / await
typescript
// jQuery 콜백 방식
$.ajax({
url: '/api/posts',
success: function (res) { render(res.items); },
error: function () { alert('실패'); },
complete: function () { hideSpinner(); }
});
// async / await 방식 (이 책의 방식)
async function loadPosts() {
setIsLoading(true);
try {
const res = await axios.get('/api/posts'); // 응답이 올 때까지 이 줄에서 대기 (브라우저는 멈추지 않음)
setPosts(res.data.items); // = success
} catch (err) {
alert('실패'); // = error
} finally {
setIsLoading(false); // = complete (성공/실패 무관 항상 실행)
}
}9) 자주 나오는 짧은 문법 (?., ??, ||)
typescript
post?.title // 옵셔널 체이닝: post가 null/undefined면 에러 대신 undefined 반환
onPageChange?.(2) // 함수가 전달되었을 때만 호출 (5장)
titleInputRef.current?.focus() // DOM 요소가 연결되었을 때만 focus 호출 (1.5장)
res.data.total ?? 0 // 널 병합: 왼쪽이 null/undefined일 때만 오른쪽 값 사용 (0이나 ''는 유지)
res.data.items || [] // OR: 왼쪽이 falsy(null, undefined, 0, '' 등)이면 오른쪽 값 사용0.4 TypeScript 최소 문법: "자바스크립트 + 타입 표시"
TypeScript는 자바스크립트에 타입 표시를 더한 언어입니다. 브라우저에서 실행되기 전에 Vite가 타입 표시를 모두 지우고 순수 자바스크립트로 바꿉니다. 그 대신 VS Code와 tsc가 코드를 실행하기 전에 오타와 잘못된 사용을 빨간 줄로 잡아 줍니다.
WARNING
npm run dev는 타입 오류가 있어도 화면을 띄웁니다. 개발 서버(Vite)는 속도를 위해 타입 표시를 검사하지 않고 지우기만 합니다. 그래서 "에디터에는 빨간 줄이 있는데 브라우저에서는 잘 돌아가는" 일이 흔합니다. 타입 오류는 ① VS Code의 빨간 줄과 ② npm run build(내부에서 tsc -b로 타입 검사 후 빌드)에서 확인하십시오. 빨간 줄을 무시하고 넘어가면 배포 빌드에서 막힙니다.
1) 변수와 함수의 타입 표시
typescript
let title: string = '공지';
let count: number = 0;
let isLoading: boolean = false;
function deletePost(id: number): void { /* ... */ } // 매개변수 id는 숫자, 반환값 없음(void)
// deletePost('1'); // ❌ 컴파일 에러: string은 number에 넣을 수 없음2) interface: 객체와 Props의 모양 정의 (Java의 DTO와 비슷)
typescript
export interface Post {
id: number;
title: string;
content?: string; // ?: 있어도 되고 없어도 되는 선택(Optional) 속성
}
interface PostRowProps {
post: Post;
onDelete: (id: number) => void; // 숫자를 받고 반환값이 없는 콜백 함수 타입
}3) 배열 타입과 제네릭(Generic) <T>
typescript
const ids: number[] = [1, 2, 3];
const [posts, setPosts] = useState<Post[]>([]); // Post 배열을 담는 상태 (1장)
const titleInputRef = useRef<HTMLInputElement>(null); // input DOM 요소를 담는 ref (1.5장)
const res = await axios.get<{ items: Post[]; total: number }>(url); // 응답 본문 모양 지정 (1장)"꺾쇠 < > 안은 담기는 데이터의 타입을 알려주는 자리"라고 기억하면 충분합니다. 우리가 직접 제네릭 컴포넌트(<BoardTable<T>>)를 만드는 방법은 8장에서 다룹니다.
4) 유니온 타입 |과 리터럴 타입
typescript
let selectedId: number | null = null; // 숫자 또는 null (1.5장)
type PostCategory = 'DEV' | 'NOTICE' | 'FREE'; // 이 세 문자열만 허용 (7장)5) 그 밖에 코드에서 마주칠 TypeScript 표시들
| 문법 | 의미 | 처음 나오는 곳 |
|---|---|---|
e.target as HTMLInputElement | 타입 단언: "이 값은 이 타입이 맞다"고 개발자가 보증 | 1.5장, 3장 |
React.FormEvent<HTMLFormElement> | React의 <form onSubmit> 이벤트 객체 타입 | 1장, 1.5장 |
React.ChangeEvent<HTMLInputElement> | React의 <input onChange> 이벤트 객체 타입 | 1.5장 |
Record<K, V> | 키가 K, 값이 V인 객체 타입 | 4장, 8장 |
unknown | "무엇인지 모름" → 쓰기 전에 typeof/Array.isArray로 확인해야 함 | 5장, 12장 |
keyof T | T의 속성 이름들의 유니온 ('id' | 'title' | ...) | 8장 |
never | 절대 도달할 수 없는 타입 (모든 분기를 다 처리했는지 검사할 때 사용) | 7장 |
IMPORTANT
타입은 실행 시점(런타임)에 사라집니다. axios.get<{ items: Post[] }>라고 써도 서버가 다른 모양을 보내면 실행 중에는 TypeScript가 막아주지 못합니다. 타입은 컴파일 타임의 약속이며, 런타임 방어 코드(|| [], 타입 가드)가 왜 함께 필요한지는 1장과 7장에서 설명합니다.
0.5 $.ajax에서 axios로
axios는 Promise 기반 HTTP 요청 라이브러리입니다. JSON 변환을 자동으로 해 주며 인터셉터(12장) 기능이 강력합니다.
| 하고 싶은 일 | jQuery | axios (이 책) |
|---|---|---|
| 목록 조회 | $.get('/api/posts', { size: 20 }, cb) | await axios.get('/api/posts', { params: { size: 20 } }) |
| 등록 | $.ajax({ url, method: 'POST', contentType: 'application/json', data: JSON.stringify(body) }) | await axios.post('/api/posts', body) (JSON 변환 자동) |
| 삭제 | $.ajax({ url: '/api/posts/' + id, method: 'DELETE' }) | await axios.delete(`/api/posts/${id}`) |
| 응답 본문 | success: function (res) { res.items } | const res = await ...; res.data.items ← **res.data**에 들어 있음 |
| 실패 처리 | error: function (xhr) {} | try { ... } catch (err) { ... } (4xx/5xx 응답도 catch로 감) |
| 요청 취소 | xhr.abort() | new AbortController() + signal: controller.signal (3장) |
0.6 React 19.3 핵심 개념 미리 보기: 무엇을, 어디서 배우는가
아래 표는 이 책에 등장하는 React 19.3 개념의 지도입니다. 처음엔 이름만 눈에 익혀 두고, 해당 장에서 자세히 배우면 됩니다.
| 개념 | 한 줄 설명 | 처음 쓰는 곳 | 원리/자세한 설명 |
|---|---|---|---|
JSX 문법 ({ }, className, map, key, onClick) | 자바스크립트로 화면 구조를 선언하고 이벤트를 연결하는 문법 | 1장 Step 1~2 | 0장 0.2, 1장 [기초 다지기] |
useState | 값이 바뀌면 컴포넌트 함수를 다시 실행해 화면을 갱신하는 상태 훅 | 1장 Step 2 | 2장, 2.5장(직접 구현) |
useEffect | 렌더링 완료 후 서버 API 호출, 타이머, 이벤트 리스너를 동기화하는 훅 | 1장 Step 2 | 1.5장 1.5.7, 3장 |
useRef | 리렌더링을 유발하지 않는 값 보관 및 실제 DOM 요소(focus, scrollIntoView) 참조 | 1.5장 1.5.1 | 1.5장, 3장 |
flushSync | React의 상태 변경 배치를 즉시 강제 커밋하여 방금 바뀐 DOM을 바로 읽는 함수 | 1.5장 1.5.2 | 1.5장 1.5.2 |
React 19 ref as a Prop | forwardRef 없이 일반 prop처럼 자식에게 ref를 바로 전달하는 표준 | 1.5장 1.5.1 | 1.5장, 8장 |
React 19 useActionState & <form action> | 폼 제출 비동기 상태(isPending, 결과, 에러)를 자동 관리하는 훅 | 1.5장 1.5.5 | 1.5장, 10장 |
| 가상 DOM(Fiber)과 자동 배치(Batching) | 메모리 상의 경량 트리로 비교(Diff) 후 최소한의 DOM만 한 번에 갱신 | 2장 | 2장 2.1~2.3 |
useMemo / useCallback & React Compiler | 계산값과 함수 참조 캐싱 및 React 19 컴파일러의 자동 메모이제이션 | 2장 2.4 | 2장 2.4, 13장 |
Stale Closure & React 19.2+ useEffectEvent | 오래된 클로저 함정과, Effect 재실행 없이 최신 값을 읽는 이벤트 분리 훅 | 3장 | 3장 3.2~3.5 |
React Router (useSearchParams, useNavigate) | URL에 따라 화면을 바꾸고 쿼리스트링에 검색 상태를 담는 라우터 | 4.5장 | 4.5장, 5장, 11장 |
Custom Hook (useXxx) | 상태를 가진 비즈니스 로직을 함수로 도려내어 재사용 및 단위 테스트 | 5장 | 5장 |
Context API & React 19 <Context> / use() | Props Drilling 없이 깊은 자식에게 서비스를 직접 주입(DI) | 6장 | 6장 |
React 19 useOptimistic | 서버 응답 전에 화면을 0.001초 만에 먼저 갱신하고 실패 시 자동 롤백 | 10장 | 10장 |
React 19.2+ <Activity> | 탭 전환 시 컴포넌트의 내부 상태와 DOM을 파괴하지 않고 숨겨 보존 | 11장, 13장 | 13장 |
0.7 사고방식 전환: "DOM을 찾아서 바꾼다" → "상태를 바꾸면 화면이 따라온다"
| 상황 | jQuery 방식 (명령형) | React 19 방식 (선언형) |
|---|---|---|
| 목록 표시 | $('#tbody').html(문자열) | posts 배열 상태 + {posts.map(p => <tr key={p.id}>...</tr>)} |
| 입력값 읽기 | $('#title').val() | value={form.title} + onChange로 연결된 상태 (또는 FormData) |
| 요소 숨기기 | $('#spinner').hide() | setIsLoading(false) + {isLoading && <div />} |
| 행 삭제 | $(this).closest('tr').remove() | setPosts(prev => prev.filter(p => p.id !== id)) |
| 건수 갱신 | $('#count').text(n) (매번 수동 갱신) | <span>총 {posts.length}건</span> (자동 계산) |
| 이벤트 연결 | $(document).on('click', '.btn', fn) | <button onClick={() => fn(post.id)}> |
가장 중요한 습관은 **"화면을 직접 건드리고 싶을 때마다, 대신 어떤 상태(useState)를 바꾸면 되는지 생각하기"**입니다. 1장에서 이 전환을 직접 손으로 체험합니다.
0.8 인터넷 예제나 레거시에서 마주치는 옛 React 코드 읽는 법
이 책은 최신 React 19.3 함수형 컴포넌트 + Hooks 표준으로 작성합니다. 하지만 검색해서 나오는 옛 블로그 글이나 사내 레거시 코드에는 Class 컴포넌트(React 16.8 이전)나 React 18 이하의 레거시 API가 남아 있을 수 있습니다. 대응 관계만 알면 당황하지 않고 읽을 수 있습니다.
tsx
// 옛날 Class 컴포넌트 (React 16.8 이전 주류)
class OldBoardApp extends React.Component {
state = { posts: [], isLoading: false }; // → useState
componentDidMount() { // → useEffect(() => { ... }, [])
this.loadPosts();
}
componentWillUnmount() { // → useEffect의 return () => { ... } 클린업
clearInterval(this.timer);
}
render() {
return <div>총 {this.state.posts.length}건</div>;
}
}| 옛날 React / React 18 이하 문법 | React 19.3 최신 표준 (이 책) |
|---|---|
class MyComp extends React.Component | function MyComp() 함수형 컴포넌트 |
this.state / this.setState({ ... }) | const [state, setState] = useState(...) |
componentDidMount / componentDidUpdate / componentWillUnmount | useEffect(() => { ...; return () => cleanup(); }, [deps]) |
React.forwardRef((props, ref) => ...) | 일반 prop으로 바로 수신: function MyInput({ ref }: { ref?: React.Ref<HTMLInputElement> }) |
<MyContext.Provider value={...}> | <MyContext value={...}> (.Provider 생략 가능, 6장) |
useContext(MyContext) | use(MyContext) (조건문 안에서도 호출 가능, 6장) |
수동 isSubmitting 상태 + onSubmit 핸들러 | useActionState + <form action={formAction}> (1.5장, 10장) |
useEffect 의존성 경고를 피하려고 eslint-disable 주석 남발 | useEffectEvent로 이벤트 로직 분리 (3장) |
🔬 심화(건너뛰어도 됨): 왜 React는 Class 컴포넌트를 버리고 함수형 + Hooks로 갔는가?
this바인딩의 혼란: 자바스크립트의this는 호출 방식에 따라 바뀌어서, 이벤트 핸들러마다this.handleClick = this.handleClick.bind(this)를 쓰거나 화살표 함수로 감싸야 했고, 비동기 콜백 안에서this.state가 예상치 못한 시점의 값을 읽는 버그가 잦았습니다.- 생명주기 메서드에 뒤엉키는 관심사: 예를 들어
componentDidMount하나 안에 "게시글 목록 API 호출"과 "창 크기 조절 리스너 등록"과 "웹소켓 연결"이 한데 섞이고, 정리는 또 저 아래componentWillUnmount에 흩어져 있어 코드를 추적하기 어려웠습니다. - 상태 로직 재사용의 어려움: 컴포넌트 간에 페이징이나 검색 로직을 재사용하려면 고차 컴포넌트(HOC)나 Render Props로 컴포넌트를 여러 겹 감싸야 했고("Wrapper Hell"), 이는 5장에서 배울 **Custom Hook(
usePagination)**으로 완벽히 해결되었습니다.
0.9 [직접 해보기] 실습 미션 💪
본격적인 1장 프로젝트에 들어가기 전에, 브라우저 개발자 도구(F12 → Console 탭)를 열고 불변성 배열 조작 3종 세트를 손으로 직접 쳐 보며 워밍업해 봅시다.
미션: 원본 배열 posts를 절대 건드리지 않고 새 배열 만들기
다음 초기 배열이 있습니다:
javascript
const posts = [
{ id: 1, title: '첫 번째 공지', likes: 0 },
{ id: 2, title: 'React 19 질문', likes: 5 }
];posts맨 앞에{ id: 3, title: '신규 글', likes: 0 }을 추가한 새 배열addedPosts를 만드십시오.posts에서id가1인 글을 제거한 새 배열deletedPosts를 만드십시오.posts에서id가2인 글의likes만1증가시킨 새 배열updatedPosts를 만드십시오.- 마지막에
console.log(posts)를 찍었을 때 원본posts는 처음 상태 그대로(길이 2, 2번 글 likes는 5) 유지되어야 합니다!
💡 미션 정답 코드 확인하기
javascript
const posts = [
{ id: 1, title: '첫 번째 공지', likes: 0 },
{ id: 2, title: 'React 19 질문', likes: 5 }
];
// 1. 맨 앞에 추가 (Spread 연산자)
const addedPosts = [{ id: 3, title: '신규 글', likes: 0 }, ...posts];
// 2. id가 1인 글 삭제 (filter)
const deletedPosts = posts.filter(p => p.id !== 1);
// 3. id가 2인 글의 likes만 +1 수정 (map + 객체 Spread)
const updatedPosts = posts.map(p =>
p.id === 2 ? { ...p, likes: p.likes + 1 } : p
);
console.log('추가 결과:', addedPosts);
console.log('삭제 결과:', deletedPosts);
console.log('수정 결과:', updatedPosts);
console.log('원본 보존 확인:', posts); // 원본은 전혀 변하지 않음!0.10 이번 장 핵심 요약 및 다음 단계 🚀
핵심 완료 체크리스트
- [ ] VS Code에 ESLint / Prettier 확장, 브라우저에 React Developer Tools 확장을 설치했다.
- [ ]
npm install은 라이브러리를 내려받고,npm run dev는 개발 서버를 켠다(타입 검사는 하지 않는다). - [ ] JSX는 HTML이 아니라 자바스크립트 함수 호출(
_jsx)로 변환되므로 태그를 반드시 닫고className을 쓴다. - [ ] React는
Object.is로 참조(메모리 주소)만 비교하므로, 상태를 바꿀 때는 원본을 고치지 않고...spread,filter,map으로 새 객체·배열을 만들어야 한다. - [ ]
async/await+try/catch/finally가$.ajax의success/error/complete에 해당하며, axios 응답 본문은res.data에 있다.
셀프 체크 — 답을 먼저 떠올린 뒤 펼쳐 보십시오.
- 에디터에 빨간 줄(타입 오류)이 있는데
npm run dev화면은 잘 뜹니다. 왜 그렇고, 어디서 막히게 될까요? posts.push(newPost); setPosts(posts);를 실행하면 왜 화면이 갱신되지 않을까요?- 다음 코드는 무엇을 출력할까요?
const res = await axios.get('/api/posts'); console.log(res.items); page가0일 때page ?? 1과page || 1은 각각 무엇이 될까요?
셀프 체크 정답
- Vite 개발 서버는 속도를 위해 TypeScript 타입 표시를 검사하지 않고 지우기만 하기 때문입니다. 타입 오류는 배포 빌드인
npm run build(내부에서tsc -b실행)에서 빌드를 멈춥니다. posts.push()는 원본 배열의 내용만 바꿀 뿐 배열의 메모리 주소(참조)는 그대로입니다.setPosts(posts)에 이전과 같은 참조를 넘기면 React는Object.is(prev, next)가true라고 보고 리렌더링을 건너뜁니다.setPosts([...posts, newPost])처럼 새 배열을 만들어 넘겨야 합니다.undefined입니다(TypeScript라면 에디터가 먼저 빨간 줄로 알려 줍니다). axios는 서버가 보낸 응답 본문을res.data에 담으므로res.data.items로 꺼내야 합니다.page ?? 1은0,page || 1은1입니다. 널 병합 연산자(??)는 왼쪽이null또는undefined일 때만 오른쪽 값을 쓰고,||는0이나''처럼 falsy인 값에도 오른쪽 값을 씁니다.
이제 준비운동이 끝났습니다. Chapter 01로 넘어가서 수년간 사내 인트라넷을 지탱해 온 저 jQuery 레거시 게시판을 직접 실행해 보고, 브라우저에서 3대 버그를 터뜨린 뒤 React 19.3으로 재탄생시켜 봅시다!