React의 기본 철학과 탄생 배경
가상 DOM (Virtual DOM) 이란?
Real DOM vs Virtual DOM 비교
가상 DOM의 패치(Patch) 과정
왜 CRA 대신 Vite인가요?
Create React App (CRA)를 표준처럼 사용했습니다. 그러나 웹팩(Webpack) 기반의 CRA는 프로젝트 규모가 커질수록 빌드 및 로컬 서버 시작 속도가 느려지는 치명적인 단점이 있습니다.➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
명령어 상세 설명
npm create vite@latest: 가장 최신 버전의 Vite 툴을 실행하여 스캐폴딩(초기 세팅)을 시작합니다.my-react-app: 생성될 프로젝트의 폴더 이름입니다. 원하는 이름으로 자유롭게 변경 가능합니다.--: npm에게 "이 뒤에 나오는 옵션들은 npm 자체가 아닌, 실행할 패키지(Vite)에게 직접 전달해라"라는 의미를 가지는 관례적인 구분자(Separator)입니다.--template react: 여러 지원 환경 중 순수 React 템플릿을 사용하여 자동 구성하겠다는 옵션 명령어입니다. (TypeScript를 쓰고 싶다면react-ts를 입력합니다)
Vite 프로젝트 폴더 구조
💡 StrictMode란 무엇인가요?
main.jsx 파일을 보면 <StrictMode> 컴포넌트가 전체 앱을 감싸고 있는 것을 볼 수 있습니다.useEffect)을 의도적으로 두 번씩 실행합니다.이는 불순물(Side Effect)이 있는 렌더링 로직이나 메모리 누수를 개발자가 빨리 발견하고 수정할 수 있도록 돕기 위함입니다. 실제 배포되는 운영(Production) 환경에서는 한 번만 렌더링되므로 안심하셔도 됩니다.
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)리액트(React)에서 컴포넌트(Component)는 UI를 구성하는 가장 기본적이고 독립적인 빌딩 블록입니다. 쉽게 말해 레고 블록을 조립하여 하나의 멋진 완성품을 만들 듯, 여러 개의 컴포넌트를 결합하여 복잡한 사용자 인터페이스를 만듭니다.
컴포넌트의 장점
- 재사용성 (Reusability): 한 번 만들어 둔 컴포넌트는 다른 화면이나 프로젝트에서 쉽게 다시 사용할 수 있습니다.
- 유지보수성 (Maintainability): 코드가 독립적으로 분리되어 있어 특정 부분에 문제가 생겼을 때 해당 컴포넌트만 수정하면 됩니다.
- 가독성 (Readability): 복잡한 HTML 구조를 의미 있는 이름(예:
<Header />,<UserProfile />)으로 추상화할 수 있습니다.
리액트(React) 생태계는 시간이 지남에 따라 크게 진화해 왔으며, 그 중심에는 클래스(Class) 컴포넌트에서 함수(Function) 컴포넌트로의 패러다임 전환이 있습니다.
과거의 주류: 클래스 컴포넌트
초기 리액트에서는 상태(State)를 가지거나 생명주기(Lifecycle) 메서드(예: componentDidMount)를 사용하려면 반드시 ES6의 class 문법을 사용해야 했습니다.
- 장점: 생명주기 메서드가 명확하게 구분되어 있어 직관적입니다.
- 단점:
this바인딩 규칙이 복잡하고 코드가 길어집니다. 로직을 재사용하기 어렵습니다(HOC, Render Props 등의 패턴 강제).
모던 리액트의 표준: 함수 컴포넌트와 Hooks
리액트 16.8 버전에서 Hooks(useState, useEffect 등)가 도입되면서, 함수 컴포넌트에서도 상태 관리와 생명주기 제어가 가능해졌습니다.
- 장점: 코드가 간결하고 읽기 쉽습니다.
this를 신경 쓸 필요가 없으며, Custom Hook을 통해 로직 재사용이 매우 쉽습니다. - 현재 상태: 리액트 공식 문서와 최신 생태계는 함수 컴포넌트를 표준으로 채택하고 있으며, 새로운 코드는 모두 함수형으로 작성하는 것을 강력히 권장합니다.
요약: 왜 함수 컴포넌트를 쓰나요?
과거 유지보수 프로젝트가 아니라면, 무조건 함수 컴포넌트와 Hooks를 사용하는 것이 최선의 선택입니다. 클래스 컴포넌트는 레거시 시스템을 이해하기 위한 배경지식으로만 알아두셔도 충분합니다.
JSX 문법의 핵심 규칙
JSX는 JavaScript 내에서 HTML과 유사한 마크업을 작성할 수 있게 해주는 문법 확장입니다.
- 반드시 하나의 부모 요소로 감싸야 합니다: 형제 노드들을 나열할 때는
<div>나<> ... </>(Fragment)로 묶어줍니다. - 자바스크립트 표현식은 중괄호
{}를 사용합니다: 변수나 함수 실행 결과를 화면에 출력할 때 사용합니다. - class 대신
className을 사용합니다: JavaScript의 예약어인 class와 겹치지 않기 위함입니다. - 태그는 반드시 닫아야 합니다:
<img />,<br />처럼 빈 태그도 꼭 닫아주어야 합니다.
// 리액트 컴포넌트는 대문자로 시작하는 자바스크립트 함수입니다.
export default function App() {
// JSX를 사용하여 화면에 보여줄 HTML을 반환(return)합니다.
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h1 style={{ color: '#61DAFB' }}>Hello, World!</h1>
<p>나의 첫 번째 리액트 컴포넌트입니다.</p>
</div>
);
}📌 1. 단방향 데이터 흐름 (One-way Data Flow)
리액트에서 데이터는 항상 위에서 아래로(부모 컴포넌트 -> 자식 컴포넌트)만 흐릅니다. 부모가 가진 데이터를 자식에게 물려주는 매개체를 Props라고 합니다.
- Props는 읽기 전용(Read-only): 자식 컴포넌트는 부모로부터 전달받은 Props를 직접 수정할 수 없습니다. UI를 그리기 위한 '설명서'처럼 다루어야 합니다.
- 상태 끌어올리기 (Lifting State Up): 자식 컴포넌트 내부에서 부모의 상태를 변경해야 하는 경우 어떻게 할까요? 위 다이어그램처럼 부모 컴포넌트가 '상태를 변경하는 함수(
setState)'를 자식에게 Props로 넘겨주고, 자식이 버튼 클릭 등의 이벤트가 발생했을 때 이 함수를 대신 호출하는 방식을 사용합니다.
📌 2. 왜 불변성(Immutability)을 지켜야 할까요?
useState를 다룰 때, 특히 객체나 배열의 경우 push()나 obj.key = value와 같이 원본 자체를 수정하면 안 됩니다. 리액트는 성능 최적화를 위해 객체의 모든 속성을 하나하나 비교하지 않고, 메모리의 주소값(참조값)이 바뀌었는지만 확인하는 '얕은 비교(Shallow Compare)'를 수행하기 때문입니다.
💡 중요:
push()를 사용해 배열에 요소를 추가하면, 배열의 내용물은 바뀌었지만 주소값은 그대로이기 때문에 리액트는 "상태가 바뀌지 않았다"고 판단하여 화면을 다시 그리지 않습니다.따라서,
...(스프레드 연산자)나filter(),map()등을 활용해 완전히 새로운 객체/배열을 만들어setState에 전달해야 합니다.
import React, { useState } from 'react';
import { UserCard } from './UserCard';
import { Container } from './Container';
import { CounterButton } from './CounterButton';
// 부모 컴포넌트 (상태 관리자)
export function PropsExample() {
// 상태 끌어올리기 (Lifting State Up)
// 자식 컴포넌트들이 공유해야 할 상태는 부모가 가지고 있어야 합니다.
const [globalCount, setGlobalCount] = useState(0);
return (
<Container title={"팀원 목록 (총 " + globalCount + "명 조회)"}>
<UserCard name="홍길동" job="프론트엔드 개발자" />
<UserCard name="김철수" /> {/* job을 안 넘기면 기본값이 들어갑니다 */}
{/* 함수를 Props로 전달하여 자식이 부모의 상태를 바꿀 수 있게 합니다 */}
<CounterButton onIncrease={() => setGlobalCount(prev => prev + 1)} />
</Container>
);
}리액트의 합성 이벤트 (Synthetic Event)
합성 이벤트(Synthetic Event) 흐름도
onClick 핸들러 등에 전달합니다.
주의: 함수 실행문을 바로 넣지 마세요!
onClick={handleClick()})을 넣으면 컴포넌트가 렌더링될 때 즉시 실행되어 무한 루프에 빠질 수 있습니다. 반드시 함수 그 자체(onClick={handleClick})를 전달하거나, 익명 화살표 함수(onClick={() => handleClick(id)}) 형태로 전달해야 합니다.
자주 쓰는 이벤트 종류
onClick: 클릭 발생 시onChange: Input 태그의 값이 변경될 때마다 (실시간 타이핑 감지)onSubmit: Form 내부의 버튼이 클릭되어 제출될 때
import React, { useState } from 'react';
export default function LoginForm() {
const [userId, setUserId] = useState('');
const handleInputChange = (e) => {
setUserId(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
alert(`로그인 시도: ${userId}`);
};
const handleSpecialClick = (msg) => {
console.log("전달받은 메시지:", msg);
};
return (
<form onSubmit={handleSubmit} style={{ padding: '20px', background: '#f8fafc', borderRadius: '8px' }}>
<label style={{ display: 'block', marginBottom: '10px' }}>
아이디:
<input
type="text"
value={userId}
onChange={handleInputChange}
placeholder="아이디를 입력하세요"
style={{ marginLeft: '10px', padding: '5px' }}
/>
</label>
<button type="submit" style={{ padding: '8px 16px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', marginRight: '10px' }}>로그인</button>
<button type="button" onClick={() => handleSpecialClick('비밀번호 찾기')} style={{ padding: '8px 16px', background: '#64748b', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
비밀번호를 잊으셨나요?
</button>
</form>
);
}1. 조건부 렌더링 (Conditional Rendering)
if문이나 for문을 직접 사용할 수 없습니다. 대신 자바스크립트의 논리 연산자를 활용하여 렌더링을 제어합니다.- 삼항 연산자 (
조건 ? 참 : 거짓): 조건에 따라 두 UI 중 하나를 명확히 선택할 때 사용합니다. (예: 로그인 버튼 / 로그아웃 버튼 스위칭) - 논리 AND 연산자 (
조건 && 참): 조건이 참일 때만 무언가를 렌더링하고, 거짓일 때는 아무것도 그리지 않을 때 사용합니다. (예: 에러 메시지 팝업, 알림창)
&& 사용 시 주의사항
0 && <div>...</div>의 경우 숫자 0이 화면에 그대로 렌더링됩니다. 배열 길이를 체크할 때는 items.length > 0 && ... 처럼 명확한 boolean 값으로 평가되도록 작성하세요!
2. 리스트 렌더링 (List Rendering)
map() 함수를 사용합니다. 데이터가 많아질수록 일일이 하드코딩할 수 없기 때문에 실무에서 가장 많이 쓰이는 패턴 중 하나입니다.key 속성을 부여해야 합니다. 리액트는 이 key를 통해 어떤 아이템이 추가/수정/삭제되었는지 빠르고 효율적으로 추적합니다. (주의: 배열의 인덱스 index를 key로 사용하면 아이템 순서가 바뀔 때 버그가 발생할 수 있으므로, 고유한 ID값을 사용하는 것이 좋습니다.)import React, { useState } from 'react';
export default function LoginGreeting() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
return (
<div style={{ padding: '20px', background: '#f8fafc', borderRadius: '8px' }}>
<h2 style={{ marginTop: 0 }}>조건부 렌더링 예제</h2>
{/* 삼항 연산자 (? :) 활용 */}
<div style={{ marginBottom: '20px' }}>
<p>상태: {isLoggedIn ? <strong style={{color: 'green'}}>로그인 됨</strong> : <strong style={{color: 'red'}}>로그아웃 상태</strong>}</p>
<button
onClick={() => setIsLoggedIn(!isLoggedIn)}
style={{ padding: '8px 16px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
>
{isLoggedIn ? '로그아웃 하기' : '로그인 하기'}
</button>
</div>
{/* 논리 AND (&&) 연산자 활용 */}
{isLoggedIn && (
<div style={{ padding: '15px', background: '#dcfce7', color: '#166534', borderRadius: '8px' }}>
환영합니다! 회원 전용 콘텐츠입니다. 👋
</div>
)}
</div>
);
}useEffect로 부수 효과(Side Effect) 관리하기
useEffect는 렌더링이 화면에 반영된 직후에 비동기적으로 이러한 작업들을 수행할 수 있게 해주는 훅입니다.컴포넌트 생명주기(Lifecycle)와 useEffect
1. Mount (생성)
useEffect가 딱 한 번 실행됩니다. 주로 백엔드 API 데이터를 불러오거나 이벤트를 등록할 때 쓰입니다.2. Update (업데이트)
3. Unmount (소멸)
의존성 배열(Dependency Array) 완벽 이해
useEffect(() => { ... }): 렌더링될 때마다 매번 실행됩니다. (거의 안 씀)useEffect(() => { ... }, []): 빈 배열. 컴포넌트가 처음 화면에 나타날 때(Mount) 단 한 번만 실행됩니다. API 초기 호출에 주로 씁니다.useEffect(() => { ... }, [상태]): 배열 안의 상태가 변경될 때마다 실행됩니다.
return으로 정리 함수를 반환하면 됩니다.useRef의 두 가지 핵심 용도
useRef는 리액트 컴포넌트 생애주기 동안 유지되는 변경 가능한 객체(.current)를 생성합니다. useState와 비슷해 보이지만 가장 큰 차이점은 값이 변경되어도 컴포넌트가 리렌더링되지 않는다는 점입니다.
- DOM 요소 직접 선택하기
특정 input에 포커스를 주거나, 스크롤 위치를 계산하는 등 바닐라 JS의getElementById처럼 실제 DOM 노드에 직접 접근해야 할 때 사용합니다. - 리렌더링을 유발하지 않는 변수 저장
타이머 ID, 이전 상태값 기억 등 값이 바뀌어도 굳이 화면을 다시 그릴 필요가 없는 내부 데이터를 보관할 때 사용합니다.
import React, { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// 1. Mount 시점에 1초마다 동작하는 타이머 세팅
console.log("타이머 시작!");
const intervalId = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// 2. Cleanup 함수 반환
// 컴포넌트가 화면에서 사라질 때(Unmount) 반드시 타이머를 해제해야 메모리 누수를 막습니다.
return () => {
console.log("타이머 정리됨");
clearInterval(intervalId);
};
}, []); // 빈 배열이므로 처음 한 번만 실행됨
return <p style={{ fontSize: '1.2rem', fontWeight: 'bold', color: '#0f172a' }}>경과 시간: {seconds}초</p>;
}
export default function App() {
const [show, setShow] = useState(true);
return (
<div style={{ padding: '20px', background: '#f8fafc', borderRadius: '8px' }}>
<h3 style={{ marginTop: 0 }}>타이머 마운트/언마운트 예제</h3>
<button
onClick={() => setShow(!show)}
style={{ padding: '8px 16px', background: show ? '#ef4444' : '#10b981', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', marginBottom: '15px' }}
>
{show ? '타이머 숨기기 (Unmount)' : '타이머 켜기 (Mount)'}
</button>
<div style={{ minHeight: '50px', padding: '15px', background: '#fff', borderRadius: '6px', border: '1px solid #e2e8f0' }}>
{show ? <Timer /> : <p style={{ color: '#64748b' }}>타이머가 화면에서 사라졌습니다.</p>}
</div>
</div>
);
}Tailwind CSS와 React의 환상적인 궁합
기존에는 CSS 파일을 별도로 작성하고 className을 지어주는 과정(BEM 방법론 등)이 매우 번거로웠습니다. Tailwind CSS는 flex, text-center, pt-4처럼 미리 정의된 유틸리티 클래스들을 조립하여 별도의 CSS 파일 없이 HTML(JSX) 안에서 직접 디자인을 완성하는 프레임워크입니다.
전통적 CSS vs Tailwind CSS
React 컴포넌트와의 완벽한 시너지
<Button />과 같은 컴포넌트로 분리하기 때문에 지저분한 클래스명을 한 곳에 캡슐화(숨김)할 수 있습니다.Tailwind 핵심 규칙
- 여백:
m-4(margin: 1rem),px-2(padding-x: 0.5rem) - 1단위는 보통 0.25rem(4px) - 색상:
bg-blue-500,text-white,border-gray-200 - 상태 및 반응형:
hover:bg-blue-600,md:flex(화면이 md 크기 이상일 때 flex 적용)
// Tailwind CSS가 설정된 환경이라고 가정합니다.
function TailwindCard() {
return (
// 부모 컨테이너: 최대 너비 설정, 중앙 정렬, 상하 여백
<div className="max-w-sm mx-auto mt-10">
{/* 카드 래퍼: 흰색 배경, 그림자, 둥근 모서리, 패딩, 테두리 */}
<div className="bg-white rounded-xl shadow-lg border border-gray-100 p-6 overflow-hidden">
{/* 태그 뱃지 */}
<span className="bg-indigo-100 text-indigo-800 text-xs font-semibold px-2.5 py-0.5 rounded-full">
New Course
</span>
{/* 제목 텍스트 */}
<h2 className="mt-3 text-xl font-bold text-gray-800">
Tailwind CSS 완벽 가이드
</h2>
<p className="mt-2 text-gray-600 text-sm leading-relaxed">
별도의 CSS 파일 없이 오직 클래스 조합만으로 아름다운 반응형 UI를 구축하는 비법을 배웁니다.
</p>
{/* 액션 버튼: flex 박스 정렬, 배경색, 호버 효과, 트랜지션 */}
<div className="mt-5 flex justify-end">
<button className="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-lg transition-colors duration-200">
수강하기
</button>
</div>
</div>
</div>
);
}React와 TypeScript의 결합
순수 자바스크립트는 런타임(브라우저 실행 중)에 에러가 터지기 전까지 문제를 모르는 경우가 많습니다. React에 TypeScript를 결합하면 컴포넌트의 Props나 State가 어떤 형태의 데이터를 받아야 하는지 미리 엄격하게 정의(Type Definition)하여 버그를 사전에 차단할 수 있습니다.
React에서 자주 쓰이는 주요 타입
React.FC: 함수형 컴포넌트 타입 지정 (최근에는 잘 쓰지 않고 매개변수에 직접 지정하는 추세)React.MouseEvent<HTMLButtonElement>: 버튼 클릭 이벤트 객체 타입React.ReactNode: children prop으로 들어올 수 있는 모든 렌더링 가능한 노드 타입
💡 이제 TypeScript는 선택이 아닌 필수
// TypeScript 환경 (.tsx 파일)
import React, { useState } from 'react';
// 1. Props의 형태를 interface로 명확히 정의합니다.
interface UserCardProps {
name: string;
age: number;
// ?를 붙이면 선택적(Optional) 속성이 됩니다.
email?: string;
isOnline: boolean;
}
// 2. 컴포넌트 매개변수에 타입을 적용합니다.
function UserProfile({ name, age, email, isOnline }: UserCardProps) {
return (
<div style={{ border: '1px solid #ccc', padding: '1rem', borderRadius: '8px' }}>
<h2>
{name} ({age}세)
<span style={{ color: isOnline ? 'green' : 'gray', marginLeft: '8px' }}>
●
</span>
</h2>
{email && <p>이메일: {email}</p>}
</div>
);
}
function App() {
// State에도 제네릭(<>) 문법으로 타입을 명시할 수 있습니다.
const [users, setUsers] = useState<UserCardProps[]>([]);
// 만약 props 타입에 맞지 않는 데이터를 넘기면 VScode 편집기에서 에디터가 빨간 줄로 경고해줍니다!
return (
<div>
<UserProfile name="Minstudio" age={28} isOnline={true} />
</div>
);
}리액트에서 폼(Form)의 상태를 효율적으로 제어하는 방식과 실무 표준인 react-hook-form 및 zod를 결합하여 복잡한 유효성 검사를 쉽고 빠르게 구현하는 방법을 배웁니다.
React Hook Form 핵심 장점
Zod를 통한 스키마 검증
z.string().email())으로 손쉽게 정의할 수 있습니다.import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// 1. Zod를 사용하여 폼의 스키마(규칙) 정의
const loginSchema = z.object({
email: z.string().email("올바른 이메일 형식을 입력해주세요."),
password: z.string().min(6, "비밀번호는 최소 6자 이상이어야 합니다."),
});
type LoginFormInputs = z.infer<typeof loginSchema>;
export default function LoginForm() {
// 2. useForm에 zodResolver를 연결하여 검증 위임
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(loginSchema),
});
const onSubmit = (data) => {
console.log("검증 성공! 전송된 데이터:", data);
alert(JSON.stringify(data, null, 2));
};
return (
<div style={{ padding: '20px', background: '#f8fafc', borderRadius: '8px' }}>
<form onSubmit={handleSubmit(onSubmit)} style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
<div>
<input
{...register("email")}
placeholder="이메일"
style={{ width: '100%', padding: '10px', borderRadius: '4px', border: '1px solid #cbd5e1' }}
/>
{/* 에러 메시지 출력 */}
{errors.email && <p style={{ color: '#ef4444', fontSize: '0.875rem', marginTop: '5px' }}>{errors.email.message}</p>}
</div>
<div>
<input
type="password"
{...register("password")}
placeholder="비밀번호"
style={{ width: '100%', padding: '10px', borderRadius: '4px', border: '1px solid #cbd5e1' }}
/>
{errors.password && <p style={{ color: '#ef4444', fontSize: '0.875rem', marginTop: '5px' }}>{errors.password.message}</p>}
</div>
<button
type="submit"
style={{ padding: '10px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontWeight: 'bold' }}
>
로그인
</button>
</form>
</div>
);
}SPA와 React Router DOM
리액트는 하나의 빈 HTML 페이지를 로드한 뒤 자바스크립트로 화면을 갈아끼우는 SPA(Single Page Application) 방식을 사용합니다. 브라우저가 실제로 새 페이지를 요청하지 않으므로 화면 깜빡임이 없고 매우 빠릅니다.
🚨 <a> 태그의 치명적 단점
<a href="..."> 태그를 사용하면 브라우저가 페이지를 완전히 새로고침합니다. 이 경우 메모리에 저장되어 있던 리액트의 모든 State(상태값)가 날아가버리므로 절대 사용해서는 안 됩니다.💡 <Link> vs useNavigate()
<Link>는 사용자가 명시적으로 클릭하는 메뉴바나 네비게이션에 사용합니다. 반면 useNavigate()는 '로그인 버튼 클릭 후 성공하면 메인으로 이동'과 같이 프로그래밍 방식(특정 로직 내부)에서 주소를 이동할 때 씁니다.하지만 사용자는 주소(URL)에 따라 다른 화면을 보고 싶어 하고, 뒤로가기 버튼도 작동해야 합니다. 이를 구현하기 위해 브라우저의 History API를 가로채어 가짜 페이지 이동을 구현하는 표준 라이브러리가 바로 react-router-dom입니다.
핵심 컴포넌트
BrowserRouter: 최상위를 감싸 라우팅 환경을 활성화합니다.Routes/Route: URL 경로(path)에 따라 보여줄 컴포넌트(element)를 매핑합니다.Link: HTML의<a>태그를 대체합니다. 페이지 새로고침 없이 URL만 변경합니다.
🚇 Context API: Props Drilling 문제 해결하기
Props Drilling의 고통과 Context API
Context API는 이러한 문제를 해결하는 리액트 내장 전역 상태 관리 도구입니다. 최상위에서 Provider로 감싸고 값을 공급하면, 하위의 어떤 컴포넌트든 깊이와 상관없이 useContext 훅을 이용해 해당 값을 바로 꺼내 쓸 수 있습니다.Props Drilling vs Context API
언제 Context API를 써야 할까?
Redux의 시대는 가고, Zustand의 시대가 왔다
Zustand의 전역 상태 구조
<Provider>로 앱을 감쌀 필요가 없으며, 깊숙한 곳에 있는 컴포넌트라도 훅(Hook)을 통해 스토어에 직접 연결(Subscribe)되어 최상의 렌더링 성능을 보장합니다.
📊 API 연동 실습: 외부 REST API를 활용한 데이터 대시보드 만들기
실전 API 데이터 연동 패턴
지금까지 배운 useState, useEffect, 비동기 fetch/axios를 총동원하여 실제 외부 API와 통신하고 데이터를 화면에 뿌려주는 과정을 실습합니다.
안정적인 데이터 페칭의 3요소
사용자 경험(UX)을 위해서는 데이터를 가져올 때 발생할 수 있는 3가지 상태를 반드시 모두 처리해야 합니다.
- 로딩 상태 (Loading): 스켈레톤이나 스피너를 보여주어 앱이 멈추지 않았음을 알림
- 성공 상태 (Success): 받아온 데이터를 바탕으로 UI 렌더링
- 실패 상태 (Error): 네트워크 오류나 서버 장애 시 에러 메시지와 재시도 버튼 제공
서버 상태(Server State) 관리의 종결자, TanStack Query
서버에서 데이터를 가져오기 위해 useState와 useEffect를 조합하면 앞서 배운 로딩 상태 관리, 에러 처리, 캐싱, 데이터 재요청 등 복잡한 코드를 직접 다 짜야 합니다. 클라이언트 상태(UI 토글 등)와 서버 상태(DB 데이터)를 분리하는 것이 모던 프론트엔드의 핵심 트렌드입니다.
TanStack Query (구 React Query)는 API 데이터를 훅 한 줄로 가져오고, 알아서 캐싱하며, 백그라운드에서 최신 데이터로 동기화(Stale-while-revalidate)해주는 강력한 라이브러리입니다.
Stale-While-Revalidate 캐싱 생명주기
Stale-while-revalidate 원리:
- 사용자가 페이지를 열면, 오래된(Stale) 캐시 데이터라도 먼저 즉시 화면에 렌더링합니다. (로딩 스피너 없음!)
- 동시에 백그라운드에서 서버로 새로운 데이터를 몰래 요청합니다.
- 서버에서 최신 데이터(Fresh)가 도착하면, 캐시를 업데이트하고 UI를 부드럽게(조용히) 갱신합니다.
UI의 제약을 넘어서는 고급 렌더링 패턴
부모 컴포넌트의 DOM 계층 구조를 벗어나 렌더링하는 createPortal을 활용한 모달(Modal) 구현법과, 부모 컴포넌트가 자식 커스텀 컴포넌트 내부의 DOM 요소에 직접 접근할 수 있게 해주는 forwardRef의 활용법을 다룹니다.
🚪 createPortal: z-index 지옥 탈출
overflow: hidden이나 z-index에 막혀 화면에 제대로 표시되지 않는 경우가 많습니다. createPortal을 사용하면 논리적인 React 트리 구조는 유지하면서, 실제 DOM은 최상단 <body> 직속으로 빼내어 렌더링하므로 이러한 CSS 충돌을 완벽히 해결할 수 있습니다.🎯 forwardRef: 컴포넌트를 관통하는 참조
ref를 전달하고 싶을 때, 기본적으로 커스텀 컴포넌트는 ref를 받지 못합니다. forwardRef로 자식 컴포넌트를 감싸주면 부모가 넘긴 ref를 내부의 실제 <input> 등의 DOM에 바로 꽂아넣어 포커스 제어 등 직접적인 조작이 가능해집니다.import React, { useRef } from 'react';
import CustomInput from './CustomInput';
import Modal from './Modal';
export default function App() {
const inputRef = useRef(null);
const focusInput = () => {
// 부모 컴포넌트에서 자식 컴포넌트 내부의 DOM 함수(focus)를 직접 호출
inputRef.current?.focus();
};
return (
<div style={{ padding: '20px', background: '#f8fafc', borderRadius: '8px' }}>
<CustomInput ref={inputRef} label="사용자 이름" />
<button
onClick={focusInput}
style={{ marginTop: '16px', background: '#3b82f6', color: 'white', padding: '8px 16px', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
포커스 이동하기
</button>
<Modal isOpen={true}>
<h2 style={{marginTop: 0}}>이것은 포탈로 열린 모달입니다!</h2>
</Modal>
</div>
);
}Custom Hooks: 나만의 레고 블록 만들기
useState와 useEffect를 조합한 비슷한 코드가 계속 반복된다면 어떻게 해야 할까요? 리액트에서는 이러한 상태 관리 로직만 따로 빼내어 재사용 가능한 함수로 만들 수 있습니다. 이를 커스텀 훅(Custom Hook)이라고 부릅니다.
🚨 커스텀 훅의 절대 규칙
use로 시작해야 합니다. (예: useFetch, useInput). 그래야만 리액트가 이 함수 내부에서 다른 Hook의 사용을 허용하고, 라이프사이클을 정상적으로 추적할 수 있습니다.메모이제이션(Memoization): 세상에서 가장 쉬운 이해
영수증으로 이해하는 메모이제이션
복잡한 수학 공식을 풀어야 한다고 상상해보세요. 어제 "745 × 382"의 정답을 힘들게 계산해서 영수증(메모장)에 적어두었습니다.
오늘 누군가 또 "745 × 382"를 물어본다면? 다시 계산할 필요 없이 영수증에 적힌 답을 그대로 읽어주면 됩니다. (이것이 useMemo 입니다!)
하지만 "745 × 383"을 물어본다면? 조건(의존성 배열)이 달라졌으므로 영수증을 버리고 새로 계산해야 합니다.
useMemo: 비싼 계산의 결과값(답안지)을 캐싱하여, 조건이 안 바뀌면 재사용합니다.useCallback: 함수 자체(계산하는 방법론)를 캐싱하여, 컴포넌트가 렌더링될 때마다 함수가 새로 생성되는 것을 막습니다.React.memo: 컴포넌트 자체를 통째로 씌워, 부모가 렌더링되어도 내가 받는 Props가 안 바뀌었으면 나는 렌더링 안 하겠다고 선언합니다.
useMemo / useCallback 동작 원리
import ResponsiveApp from './ResponsiveApp';
export default function App() {
return (
<div style={{ padding: '20px', background: '#f8fafc', borderRadius: '8px' }}>
<ResponsiveApp />
</div>
);
}버그 없는 UI를 향한 첫걸음, 프론트엔드 테스팅
안정적인 프론트엔드 애플리케이션을 보장하기 위한 단위 테스트(Unit Test) 작성법입니다. 가장 빠르고 현대적인 Vitest와 실제 사용자의 동작을 모방하는 React Testing Library(RTL)를 사용하여 견고한 컴포넌트를 설계합니다.
⚡ Vitest: Vite 기반의 압도적인 속도
👤 RTL: 사용자 중심의 테스트 철학
import React from 'react';
import Counter from './Counter';
export default function App() {
return (
<div style={{ padding: '20px', background: '#f8fafc', borderRadius: '8px', textAlign: 'center' }}>
<h2 style={{ marginTop: 0 }}>Vitest 테스트 대상 컴포넌트</h2>
<p style={{ color: '#64748b', marginBottom: '20px' }}>아래의 카운터 컴포넌트가 테스트 대상입니다.</p>
<Counter />
</div>
);
}