이 글에서 정리하는 내용
React 컴포넌트를 어떻게 이해해야 하는지부터, UI를 왜 나누는지, 재사용 가능한 컴포넌트를 어떻게 설계하는지, 그리고 presentational / container 느낌의 역할 분리를 어떻게 받아들이면 좋은지까지 한 흐름으로 정리합니다. 버튼, 카드, 모달, 리스트 같은 익숙한 UI를 기준으로 생각하면 React 컴포넌트가 훨씬 덜 추상적으로 느껴집니다.
- 이 글에서 정리하는 내용
- React 컴포넌트를 어떻게 이해하면 되는가
- UI를 왜 컴포넌트로 나누는가
- 재사용 가능한 컴포넌트는 어떻게 설계하는가
- presentational / container 느낌으로 역할을 나누는 방법
- 결론
- 많이 받는 질문
React 컴포넌트를 어떻게 이해하면 되는가

처음 React를 볼 때는 컴포넌트를 파일 하나나 함수 하나 정도로만 받아들이기 쉽습니다. 하지만 React 공식 문서 기준으로 보면 React 컴포넌트는 마크업을 반환하는 일반 JavaScript 함수로 이해하는 편이 더 정확합니다. 버튼 하나도 컴포넌트가 될 수 있고, 카드 하나도 컴포넌트가 될 수 있으며, 상황에 따라 페이지 전체도 하나의 큰 컴포넌트가 될 수 있습니다.
중요한 점은 크기가 아니라 역할입니다. 화면에서 어떤 부분이 스스로 의미를 가지는지, 반복될 가능성이 있는지, 다른 화면에서도 비슷한 형태로 다시 등장할 수 있는지를 기준으로 보면 컴포넌트라는 개념이 훨씬 선명해집니다. React는 화면을 통째로 만드는 방식보다 작은 UI 조각을 조합하는 방식에 더 가깝고, React 컴포넌트는 바로 그 조각을 표현하는 단위입니다.
버튼도 컴포넌트, 카드도 컴포넌트
type ButtonProps = {
label: string;
};
export default function MyButton({ label }: ButtonProps) {
return <button>{label}</button>;
}
이 예시는 단순하지만 핵심을 잘 보여줍니다. React에서는 버튼 하나도 컴포넌트가 될 수 있습니다. 즉, React 컴포넌트는 거창한 단위가 아니라, 화면 안에서 의미 있는 조각이라면 충분합니다. 이 감각이 잡히면 페이지를 볼 때도 한 장짜리 화면이 아니라 여러 컴포넌트가 모인 구조로 보이기 시작합니다.
UI를 왜 컴포넌트로 나누는가
UI를 컴포넌트로 나누는 가장 큰 이유는 재사용성과 유지보수성 때문입니다. 같은 버튼 스타일이 여러 곳에 반복된다면 한 번 정의해 두고 여러 곳에서 쓰는 편이 훨씬 안정적입니다. 나중에 디자인이 바뀌어도 한 군데를 바꾸면 전체 반영이 쉬워집니다.
또한 나누는 과정 자체가 화면 구조를 더 명확하게 만들어 줍니다. 헤더, 상품 카드, 목록, 필터 영역, 모달처럼 역할별로 나누면 어느 부분이 무엇을 담당하는지 읽기 쉬워지고, 수정 범위도 예측하기 좋아집니다. 결국 컴포넌트 분리는 코드 정리를 위한 취향 문제가 아니라, 변경에 덜 흔들리는 구조를 만드는 방법에 가깝습니다.
리스트와 아이템을 분리해서 생각하기
type Product = {
id: number;
name: string;
price: number;
};
function ProductItem({ product }: {
product: Product;
}) {
return (<li> <strong>{product.name}</strong> - {product.price}원 </li>);
}
export default function ProductList({ products }: {
products: Product[];
}) {
return (<ul> {products.map((product) => (<ProductItem key={product.id} product={product}/>))} </ul>);
}
이 구조에서는 목록 전체를 렌더링하는 책임과 항목 하나의 모양을 그리는 책임이 분리됩니다. 당장은 한 파일 안에 있어도 괜찮지만, 역할이 분리되어 있다는 점이 중요합니다. 나중에 상품 카드 디자인이 복잡해져도 ProductItem을 중심으로 수정하면 되고, 목록 정렬이나 필터링 로직은 ProductList 쪽에서 고민하면 됩니다. 이런 분해 방식은 React 컴포넌트를 설계할 때 가장 자주 쓰는 사고방식입니다.
| 나누지 않았을 때 | 나눴을 때 |
|---|---|
| 반복, 레이아웃, 항목 모양이 한곳에 섞임 | 리스트와 아이템의 책임이 구분됨 |
| 수정 범위를 한 번에 파악하기 어려움 | 어디를 수정해야 할지 비교적 명확함 |
재사용 가능한 컴포넌트는 어떻게 설계하는가
재사용 가능한 컴포넌트를 만든다는 것은 비슷해 보이는 UI를 억지로 하나로 합친다는 뜻이 아닙니다. 핵심은 공통 구조와 변경 포인트를 구분하는 것입니다. 버튼이라면 공통 뼈대는 버튼이고, 달라지는 부분은 텍스트, 색상, 클릭 동작일 수 있습니다. 카드라면 카드 틀은 같지만 제목, 설명, 이미지, 버튼 유무가 바뀔 수 있습니다. 재사용 가능한 React 컴포넌트를 설계할 때는 무엇을 고정하고 무엇을 바깥에서 받는지 먼저 구분하는 편이 좋습니다.
이때 props는 바뀌는 값을 밖에서 주입받는 통로입니다. 다만 재사용을 욕심내다 보면 props가 너무 많아져서 오히려 쓰기 어려운 컴포넌트가 되기도 합니다. 그런 경우에는 정말 같은 컴포넌트가 맞는지 다시 보는 것이 좋습니다. 겉모양만 비슷하고 역할이 다르면 분리하는 편이 더 낫습니다.
공통 구조와 변경 포인트를 분리한 카드 예시
type UserCardProps = {
name: string;
description: string;
buttonLabel?: string;
};
export default function UserCard({ name, description, buttonLabel = '자세히 보기' }: UserCardProps) {
return (<article> <h4>{name}</h4> <p>{description}</p> <button>{buttonLabel}</button> </article>);
}
이런 형태는 여러 화면에서 반복하기 좋습니다. 중요한 것은 처음부터 모든 상황을 다 커버하려 하지 않는 것입니다. 우선 실제로 반복되는 부분만 공통화하고, 이후 반복 패턴이 명확해질 때 범위를 넓히는 편이 훨씬 안전합니다. 재사용 가능한 컴포넌트는 많은 값을 받는 컴포넌트보다 읽기 쉽고 쓰기 쉬운 컴포넌트가 더 좋은 경우가 많습니다.
열림 여부를 밖에서 제어하는 패널 구조
type PanelProps = {
isOpen: boolean;
title: string;
onClose: () => void;
};
export default function Panel({ isOpen, title, onClose }: PanelProps) {
if (!isOpen)
return null;
return (<div> <h3>{title}</h3> <button onClick={onClose}>닫기</button> </div>);
}
이 패널은 열림 여부와 닫기 요청을 설명하는 예시입니다. 실제 모달의 포커스 이동·포커스 가두기·Escape 처리까지 구현한 코드는 아닙니다. 모달이 스스로 모든 상태를 다 들고 있으려고 하면 다른 화면에서 제어하기 어려워집니다. 반대로 열림 여부와 닫기 동작을 바깥에서 받고, 모달 내부는 보여주는 역할에 집중하면 다양한 곳에서 재사용하기 쉬워집니다. 여러 컴포넌트가 같은 열림 상태를 함께 써야 할 때는 가장 가까운 공통 부모로 상태를 올리고 props로 내려주는 방식이 자연스럽습니다.
presentational / container 느낌으로 역할을 나누는 방법

React를 공부하다 보면 presentational 컴포넌트와 container 컴포넌트라는 표현을 자주 보게 됩니다. 지금은 이 패턴을 엄격하게 그대로 따르기보다, React 컴포넌트의 관심사를 분리하는 힌트 정도로 받아들이면 충분합니다. 쉽게 말해 presentational 쪽은 화면을 어떻게 보여줄지에 집중하고, container 쪽은 어떤 데이터를 가져오고 어떤 상태를 관리할지에 집중합니다.
예전에는 이 분리를 컴포넌트 단위로 많이 했지만, 요즘은 커스텀 훅을 이용해 로직을 분리하는 방식도 자주 사용합니다. 다만 커스텀 훅은 상태 자체를 여러 컴포넌트가 함께 쓰게 만드는 도구가 아니라, 상태를 다루는 로직을 재사용하는 도구로 이해하는 편이 정확합니다. 결국 이름보다 중요한 것은 UI를 그리는 코드와 데이터 처리 코드를 뒤섞지 않는 감각입니다.
UI 전용 컴포넌트와 로직 분리 예시
type ProfileCardProps = {
name: string;
email: string;
};
function ProfileCard({ name, email }: ProfileCardProps) {
return (<section> <h4>{name}</h4> <p>{email}</p> </section>);
}
function getProfile() {
return { name: '김리액트', email: 'react@example.com' };
}
export default function ProfileCardContainer() {
const profile = getProfile();
return <ProfileCard name={profile.name} email={profile.email}/>;
}
이 예시에서는 ProfileCard가 화면을 그리고, getProfile과 ProfileCardContainer가 데이터를 준비해 전달합니다. 이렇게 나누면 디자인을 수정할 때는 UI 쪽만 보면 되고, 데이터 출처가 바뀔 때는 로직 쪽만 보면 됩니다. 컴포넌트를 잘게 나누는 목적도 결국 이런 식으로 책임을 선명하게 만드는 데 있습니다.
결론
React에서 컴포넌트를 공부할 때 가장 먼저 잡아야 하는 감각은 화면을 작은 UI 조각으로 본다는 점입니다. 그리고 그 조각을 나누는 기준은 파일 개수나 크기가 아니라 역할입니다. 버튼, 카드, 모달, 리스트처럼 의미 있는 단위로 쪼개고, 공통 구조와 변경 포인트를 구분하면 재사용 가능한 React 컴포넌트를 훨씬 안정적으로 설계할 수 있습니다.
presentational / container 패턴의 이름을 외우기보다 UI를 그리는 책임과 상태·데이터를 다루는 책임을 적절히 분리하는 감각을 익히는 편이 중요합니다. 이 흐름이 익숙해지면 상태 끌어올리기와 커스텀 훅 같은 주제도 자연스럽게 이어집니다.
컴포넌트를 나눈 뒤 데이터 전달은 React props 데이터 전달, 컴포넌트 내부 상태 기준은 React useState 상태 관리 기본에서 이어서 정리할 수 있습니다.
많이 받는 질문
Q. 컴포넌트는 무조건 잘게 나눌수록 좋은가요?
아닙니다. 너무 잘게 나누면 오히려 파일만 많아지고 흐름이 끊길 수 있습니다. 중요한 기준은 역할 분리와 읽기 쉬움입니다. 한 컴포넌트가 여러 책임을 동시에 가지기 시작할 때 나누는 쪽이 더 자연스럽습니다.
Q. 재사용하려다 props가 너무 많아지면 어떻게 해야 하나요?
먼저 정말 같은 컴포넌트인지 다시 확인하는 것이 좋습니다. 겉모양만 비슷하고 실제 역할이 다르면 분리하는 편이 더 낫습니다. 공통화는 많이 받는 것보다, 실제로 반복되는 패턴만 정확히 묶는 것이 더 중요합니다.
Q. presentational / container 패턴은 지금도 꼭 써야 하나요?
엄격하게 따를 필요는 없습니다. 다만 화면 표시와 데이터 처리의 책임을 나눈다는 생각은 지금도 유효합니다. 요즘은 container 컴포넌트 대신 커스텀 훅으로 로직을 분리하는 방식도 많이 사용합니다.
Q. 커스텀 훅을 쓰면 여러 컴포넌트가 같은 상태를 자동으로 공유하나요?
아닙니다. 커스텀 훅은 상태를 다루는 로직을 재사용하게 해주지만, 각 호출은 서로 독립적입니다. 여러 컴포넌트가 같은 상태를 함께 써야 한다면 공통 부모로 상태를 올리거나, 상황에 따라 Context 같은 다른 공유 방식이 필요합니다.
공식 문서
선언 위치와 함수 이름을 확인하세요
컴포넌트 이름은 대문자로 시작하고 <ProductItem />으로 사용합니다. ProductItem()을 직접 호출하는 방식으로 컴포넌트 렌더링을 대신하지 않습니다. 자식 함수 정의도 부모 함수 바깥, 모듈 최상위에 두어 렌더마다 다른 컴포넌트 타입이 생기지 않게 합니다.
한 파일에 여러 컴포넌트가 있어도 됩니다. 분리할 때는 default export와 이름 있는 export에 맞춰 import를 작성하세요. JSX를 가진 파일은 .jsx 또는 .tsx로 저장합니다. 위 타입 예시는 .tsx 발췌이고 아래 다운로드는 TypeScript 선수 지식 없이 실행할 수 있는 .jsx입니다.
getProfile은 고정 객체를 반환하므로 일반 함수입니다. 내부에서 Hook을 쓰지 않는 함수에 use 접두사를 붙여 커스텀 Hook처럼 표시할 이유가 없습니다. 실제 상태 로직을 Hook으로 추출해도 여러 호출의 state가 자동 공유되는 것은 아닙니다.
직접 실행할 실습
starter의 App.jsx에 함께 있는 ProductList와 ProductItem을 각 파일로 옮기고 default export/import를 연결하세요. ProductList는 반복, ProductItem은 한 항목 표현만 담당하게 하세요.
아래는 완성본의 src/App.jsx입니다. ZIP에는 별도 HTML·CSS, 진입점, 필요한 자식 컴포넌트, 고정 패키지 버전과 테스트가 함께 들어 있습니다. starter는 미완성 동작을 수정하는 시작본이고 complete는 비교용 완성본입니다.
실습 파일
파일 경로를 확인하고 같은 프로젝트 안에 저장하세요. 이미지 등 소스 목록에 없는 파일과 실행 안내는 실습 ZIP에 포함되어 있습니다.
목록과 항목 컴포넌트를 분리하고 부모에서 자식으로 필요한 값을 전달합니다. 수정 전은 시작 상태, 수정 후는 비교할 완성 상태입니다.
수정 전
index.html
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React 실습</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
package.json
{
"name": "blogflow-react-2282",
"version": "1.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=22.12.0"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run"
},
"dependencies": {
"react": "19.3.0",
"react-dom": "19.3.0"
},
"devDependencies": {
"vite": "8.3.0",
"vitest": "4.1.11",
"jsdom": "30.0.1",
"@testing-library/react": "16.3.3",
"@testing-library/user-event": "14.6.7"
}
}
src/App.jsx
import React from 'react';
const products = [
{ id: 'pen', name: '연필', price: 1000 },
{ id: 'book', name: '노트', price: 3000 },
];
function ProductItem({ product }) {
return (
<li>
<strong>{product.name}</strong> · {product.price.toLocaleString('ko-KR')}원
</li>
);
}
function ProductList() {
return (
<ul>
{products.map((product) => (
<ProductItem key={product.id} product={product} />
))}
</ul>
);
}
export default function App() {
return (
<main>
<h1>작은 문구점</h1>
<ProductList />
</main>
);
}
src/main.jsx
import React, { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './style.css';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
);
src/style.css
* {
box-sizing: border-box;
}
body {
margin: 0;
color: #222;
background: #fff;
font-family: sans-serif;
line-height: 1.6;
}
main {
width: min(100% - 32px, 760px);
margin: 32px auto;
}
section,
article {
padding: 16px;
border: 1px solid #aaa;
margin: 16px 0;
}
button,
input,
select {
font: inherit;
color: inherit;
background: #fff;
border: 1px solid #777;
padding: 8px 12px;
min-height: 44px;
}
button {
margin: 4px;
}
button:disabled {
color: #666;
background: #eee;
}
input,
select {
display: block;
width: 100%;
margin: 4px 0 16px;
}
:focus-visible {
outline: 2px solid #222;
outline-offset: 3px;
}
li {
margin: 8px 0;
}
test/App.test.jsx
import React from 'react';
import { test, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from '../src/App.jsx';
test('renders both product items through the extracted components', () => {
render(<App />);
expect(screen.getAllByRole('listitem')).toHaveLength(2);
expect(screen.getByText('연필')).toBeTruthy();
expect(screen.getByText(/3,000원/)).toBeTruthy();
});
test/setup.js
import { afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
afterEach(cleanup);
vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
test: { environment: 'jsdom', setupFiles: './test/setup.js' },
});
수정 방법
index.html
이 파일은 변경하지 않습니다. 다른 파일을 수정하는 동안 기존 내용을 유지하세요.
package.json
이 파일은 변경하지 않습니다. 다른 파일을 수정하는 동안 기존 내용을 유지하세요.
src/App.jsx
src/App.jsx에서 아래 차이가 있는 위치를 수정하세요. -는 삭제할 줄, +는 추가할 줄이며 표시는 실제 코드에 넣지 않습니다.
목록과 항목 컴포넌트를 분리하고 부모에서 자식으로 필요한 값을 전달합니다.
--- 수정 전/src/App.jsx
+++ 수정 후/src/App.jsx
@@ -1,24 +1,6 @@
import React from 'react';
-const products = [
- { id: 'pen', name: '연필', price: 1000 },
- { id: 'book', name: '노트', price: 3000 },
-];
-function ProductItem({ product }) {
- return (
- <li>
- <strong>{product.name}</strong> · {product.price.toLocaleString('ko-KR')}원
- </li>
- );
-}
-function ProductList() {
- return (
- <ul>
- {products.map((product) => (
- <ProductItem key={product.id} product={product} />
- ))}
- </ul>
- );
-}
+import ProductList from './ProductList.jsx';
+
export default function App() {
return (
<main>
수정 후 탭의 전체 파일과 비교한 뒤 본문의 확인 절차를 실행하세요.
src/ProductItem.jsx
src/ProductItem.jsx 경로에 새 파일을 만드세요. 수정 후 탭에서 전체 코드를 복사할 수 있습니다.
목록과 항목 컴포넌트를 분리하고 부모에서 자식으로 필요한 값을 전달합니다.
src/ProductList.jsx
src/ProductList.jsx 경로에 새 파일을 만드세요. 수정 후 탭에서 전체 코드를 복사할 수 있습니다.
목록과 항목 컴포넌트를 분리하고 부모에서 자식으로 필요한 값을 전달합니다.
src/main.jsx
이 파일은 변경하지 않습니다. 다른 파일을 수정하는 동안 기존 내용을 유지하세요.
src/style.css
이 파일은 변경하지 않습니다. 다른 파일을 수정하는 동안 기존 내용을 유지하세요.
test/App.test.jsx
이 파일은 변경하지 않습니다. 다른 파일을 수정하는 동안 기존 내용을 유지하세요.
test/setup.js
이 파일은 변경하지 않습니다. 다른 파일을 수정하는 동안 기존 내용을 유지하세요.
vite.config.js
이 파일은 변경하지 않습니다. 다른 파일을 수정하는 동안 기존 내용을 유지하세요.
수정 후
index.html
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React 실습</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
package.json
{
"name": "blogflow-react-2282",
"version": "1.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=22.12.0"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run"
},
"dependencies": {
"react": "19.3.0",
"react-dom": "19.3.0"
},
"devDependencies": {
"vite": "8.3.0",
"vitest": "4.1.11",
"jsdom": "30.0.1",
"@testing-library/react": "16.3.3",
"@testing-library/user-event": "14.6.7"
}
}
src/App.jsx
import React from 'react';
import ProductList from './ProductList.jsx';
export default function App() {
return (
<main>
<h1>작은 문구점</h1>
<ProductList />
</main>
);
}
src/ProductItem.jsx
import React from 'react';
export default function ProductItem({ product }) {
return (
<li>
<strong>{product.name}</strong> · {product.price.toLocaleString('ko-KR')}원
</li>
);
}
src/ProductList.jsx
import React from 'react';
import ProductItem from './ProductItem.jsx';
const products = [
{ id: 'pen', name: '연필', price: 1000 },
{ id: 'book', name: '노트', price: 3000 },
];
export default function ProductList() {
return (
<ul>
{products.map((product) => (
<ProductItem key={product.id} product={product} />
))}
</ul>
);
}
src/main.jsx
import React, { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './style.css';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
);
src/style.css
* {
box-sizing: border-box;
}
body {
margin: 0;
color: #222;
background: #fff;
font-family: sans-serif;
line-height: 1.6;
}
main {
width: min(100% - 32px, 760px);
margin: 32px auto;
}
section,
article {
padding: 16px;
border: 1px solid #aaa;
margin: 16px 0;
}
button,
input,
select {
font: inherit;
color: inherit;
background: #fff;
border: 1px solid #777;
padding: 8px 12px;
min-height: 44px;
}
button {
margin: 4px;
}
button:disabled {
color: #666;
background: #eee;
}
input,
select {
display: block;
width: 100%;
margin: 4px 0 16px;
}
:focus-visible {
outline: 2px solid #222;
outline-offset: 3px;
}
li {
margin: 8px 0;
}
test/App.test.jsx
import React from 'react';
import { test, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from '../src/App.jsx';
test('renders both product items through the extracted components', () => {
render(<App />);
expect(screen.getAllByRole('listitem')).toHaveLength(2);
expect(screen.getByText('연필')).toBeTruthy();
expect(screen.getByText(/3,000원/)).toBeTruthy();
});
test/setup.js
import { afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
afterEach(cleanup);
vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
test: { environment: 'jsdom', setupFiles: './test/setup.js' },
});
src/ProductList.jsx
src/ProductItem.jsx
기대 결과: 연필 1,000원, 노트 3,000원이 목록에 나타납니다. 항목 컴포넌트 한 곳을 수정하면 두 항목 표현이 함께 바뀝니다.
압축을 풀고 complete에서 npm install 후 npm run dev를 실행합니다. 빌드·검증은 별도 터미널에서 npm test, npm run build로 실행합니다. 자세한 시작본 과제와 파일 설명은 README에 있습니다.
실습의 공식 참고 문서
이 글이 도움이 되었나요?
React 학습 순서
필수 19개 · 전체 23개
읽음 기록 관리
전체 과정 목차 (23개)
- 필수 길잡이 · React 학습 순서: 컴포넌트·props·state부터 상태관리까지
- 필수 학습 · React Vite 사용법: Vite 8 프로젝트 생성·실행·빌드
- 필수 학습 · React 컴포넌트 개념 정리: UI 재사용 구조 잡기 현재 글
- 필수 학습 · React JSX 문법 사용법: 조건부 렌더링과 리스트 처리
- 필수 학습 · React props 사용법: 부모에서 자식으로 데이터 전달하는 구조
- 필수 학습 · React useState 사용법: state와 객체 배열 업데이트 기준
- 선택 참고 · React state 업데이트 안됨 문제 해결
- 필수 선수 · React 리스트 key 경고 해결 기준: index key를 피해야 하는 이유
- 필수 학습 · React 제어 컴포넌트 폼과 상태 끌어올리기 첫 실습 가이드
- 필수 학습 · React useRef 실습: 입력 포커스와 state의 역할 나누기
- 필수 선수 · React useEffect 두 번 실행되는 이유: StrictMode·API 중복 해결
- 선택 참고 · React Maximum update depth exceeded 오류 해결: 무한 렌더링 원인 찾기
- 선택 참고 · React Cannot update 오류 해결: 렌더링 중 setState 원인
- 필수 학습 · React useReducer와 Context 실습: 작업 목록 상태를 여러 컴포넌트에서 공유하기
- 필수 학습 · React 컴포넌트 props 타입 지정하기: 부모와 자식 사이의 값 구조 잡기
- 선택 참고 · React Hook Form 에러 메시지 표시 문제 해결: validation이 안 보일 때 체크리스트
- 필수 길잡이 · React 라이브러리 조합 가이드: Zustand·TanStack Query·shadcn/ui 선택 기준
- 필수 학습 · Next.js 커스텀 훅 설계: 프론트엔드 상태 관리 구조 잡기
- 필수 학습 · React Compiler 기준: useMemo useCallback 언제 줄일까
- 필수 학습 · React·TypeScript 검색 필터 만들기: 상태와 결과 목록 연결
- 필수 학습 · React Router v7 실습: BrowserRouter부터 Layout·Outlet·상세 경로까지
- 필수 학습 · shadcn/ui 시작 실습: Vite 설정·components.json·입력 폼·Sonner
- 필수 학습 · shadcn/ui 복합 컴포넌트 실습: Dialog·Popover·Carousel과 접근성
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.