React
These Study Room materials were written and edited by keysub with the assistance of AI.
index- Expand / Collapse
1.react project creation 2.React Basics 1.Folder structure and description 1. Individual file description 2. Difference between JavaScript and TypeScript files 2.Component 1. Creating a screen with components in SPA 2. Why does the component name start with a capital letter? 3. component is used like a tag in JSX 4. export and import 5. Return value of function component 6. Function component and class component 7. JSX Grammar and Rules 1. Convert JSX and React.createElement 2. Caution about escaping JSX and HTML insertion 3. Write annotation in JSX 8. Separation and combination of components 3.Condition, branch, repeat processing 1. Conditional Rendering 2. List Rendering 1. List rendering key 3.Component Development 1.Re-rendering timing 2. props 1. data type of props 2. Decomposition of props structure and default value 3. Function props and event processing 4. children 3.state default useState 1. Functional update 2. State immutability 4.Event Handling 1.React events and handlers 2.Event propagation 1. Capture and Bubble Stage 2. Stop propagation with stopPropagation 3.Cancel default action with preventDefault 5.Form 1.Form input value management 1. What is the difference between target and current? 2. Control component 3. Uncontrolled component 2.Example of form implementation according to input elements 1. Managing input elements as state 2. Nested object state update 3. array state update 4. Update nested state with Immer 3. React Hook Form 1. Install 2. Basic usage 6.Hook Utilization 1.useState: state and setter 2.useEffect: Synchronize with external system 3.useRef: Value reference separate from rendering 4.useContext: Passing values to deep components 5.useReducer: Complex state update 6.Performance Optimization Hook 7.useId: Create accessibility ID 8.Prioritized UI updates 9.Action Hook in React 19 7.Embedded Component 1. Fragment 2. StrictMode 3. Profiler 4. Suspense 5.Lazy loading components 6.Import data via network 7.Loading·Error·Empty result processing 8.Style in component development 1. Styled JSX 2. styled-components 3. Emotion 9.React Router 1. SPA 2.Placement of components without a router 3.Deployment using a router 4.Path parameters 5.Go to page with Link 6.Show your current location with NavLink 10.Portal 1.Separating DOM locations with Portal 2.Error Handling rendering errors with Boundary 11.Use the React library 1.Construct UI with MUI Material 2.Developing components in Storybook 3.Manage server status with TanStack Query 12.Test 1.Types of tests 2.Select Test Tool 3.Choosing tests in practice
1. Create react project - Expand / Collapse
Create React App (CRA) is the old standard way to create React projects, supporting native JavaScript projects and TypeScript templates. When creating a project, necessary packages are also installed.
This course uses Vite and TypeScript. Vite is a fast and lightweight development server and build tool.
| Step | Instruction and result |
|---|---|
| 1. Create project | npm create vite@latest 프로젝트이름 -- --template react-ts |
| 2. Program installation | npm install |
| 3. Turn on (run) the server | npm run dev |
| 4. When running build | npm run build , a dist/ folder is created (can be checked asnpm run preview ) |
2. React Basics - Expand / Collapse
React projects are installed with libraries that create screens and tools that help with development and build. You can check the package and execution command at package.json.
| Category | Description | Yes |
|---|---|---|
dependencies |
Libraries required to run the app | react, react-dom |
devDependencies |
Tools used for development, type checking, and build | vite, typescript, @vitejs/plugin-react |
scripts |
Items to register frequently executed commands | npm run dev, npm run build |
npm installinstalls the package declared in package.json, and package-lock.jsonrecords the installed package version, reducing differences between development environments.
2.1 Folder structure and description- Expand / Collapse
A basic project created with Vite, React, and TypeScript templates has the following structure. Files and folders may vary depending on project settings or add-ons.
프로젝트/
├── public/ 정적 파일 (빌드 과정에서 그대로 복사)
├── src/ React 애플리케이션 소스
│ ├── assets/ 컴포넌트에서 사용하는 이미지 등의 자원
│ ├── App.tsx 최상위 애플리케이션 컴포넌트
│ ├── main.tsx 애플리케이션 진입점
│ └── ... 기능별 컴포넌트와 스타일
├── index.html 브라우저가 처음 받아 보는 HTML
├── package.json 패키지와 npm 명령
├── package-lock.json 설치 패키지 버전 기록
├── tsconfig*.json TypeScript 설정
└── vite.config.ts Vite 설정
2.1.1 Individual file description
2.1.1.1 Start page: index.html
In the Vite project, index.htmlis in the project root, not in public/ . This is the HTML that the browser will display first, and specifies the DOM element where React will place the screen and the TypeScript entry file. On the other hand, in the basic structure of Create React App (CRA), the starting HTML was at public/index.html.
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
public/, place a static file that will be provided directly to the specified URL rather than importin the app code. For example, you could have a favicon (favicon.ico), a search engine crawl rules file (robots.txt), a PWA manifest (manifest.webmanifest), an image to use as a direct path, etc.
public/
├── favicon.ico
├── robots.txt
└── images/
└── logo.png
Vite copies the files in public/ to the build result, dist/, while maintaining the path and file name. Therefore, public/images/logo.pngcan be referenced by the path /images/logo.png in your app.
<img src="/images/logo.png" alt="로고" />
Images to be used directly as importin the component are usually placed in src/assets/. These files are processed during Vite's build process, and the resulting file name or path may vary. In summary, public/is suitable for files whose URL and file name must be kept as is, and src/assets/is suitable for resources imported from source code and used.
2.1.1.2 src/main.tsx: App entry point and rendering
index.htmlThis was importedmain.tsxis the starting point for a React app. This file is#rootFind the element, create a React root, and create the top-level component within it.<App />Render .
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
createRootsets the DOM area that React will manage, and renderdisplays the React elements passed to that area on the screen. <StrictMode>is a check function that helps find potential problems during development, and does not create a separate UI visible to the user.
2.1.1.3 src/App.tsx: Top-level component
App.tsxis a starting component that combines the app's main screen and sub-components. It returns JSX, and when main.tsxrenders <App />, React reflects Appand the components below it on the screen.
function App() {
return (
<main>
<h1>안녕하세요, React!</h1>
</main>
);
}
export default App;
Rendering is the process of calculating the screen based on the JSX returned by the component and reflecting it in the DOM. Not only when you first start the app, but also when there are changes that affect the screen, such as when statechanges, React runs the component again to update the necessary parts. There is a difference between re-executing a component function and recreating the entire browser DOM. For information on when re-rendering occurs, please refer to the re-rendering timing in Chapter 3, “Component Development.”.
The execution flow can be summarized as follows.
index.html → src/main.tsx → src/App.tsx → 화면에 표시
2.1.2 Differences between JavaScript and TypeScript files
JavaScript files usually use the extension .js or .jsx, and TypeScript files use the .ts or .tsx extension. React components that use JSX syntax are written as .jsxand .tsx, respectively.
-TypeScript: Type checking during development allows you to check for incorrect data types in advance. -JavaScript: Since there is no separate type check, problems with mismatched data types can only be discovered during execution.
TypeScript's type notation is removed and converted to JavaScript during the build process. After all, browsers execute JavaScript.
2.2 What is a component?- Expand / Collapse
Components are reusable units that make up a screen. Everything from small UI such as buttons or titles to entire pages can be created with components, and the screen is composed by combining multiple components.
In this lesson, we will use the function component, which defines a component as a function. Function components return JSX, and component names start with an uppercase letter.
2.2.1 Creating screens with components in SPA
React apps usually operate in a SPA (Single-Page Application) manner. In traditional multi-page websites, when you navigate to a different page, the browser often requests a new HTML document from the server, and then re-displays the screen with the document it received. SPA initially loads an HTML document and then configures the screen by rendering or replacing necessary components within the app, rather than re-requesting the entire document each time the screen changes.
For example, when moving from a list screen to a detail screen, you can lower the list component and render the detail component, or change only part of the screen by combining the two components. Screen switching can be implemented by having the router select the component that matches the current address. However, SPA can also request data from the server, and can request a new document when refreshing the browser or moving to an external page.
function Greeting() {
return <h1>안녕하세요, React!</h1>;
}
2.2.2 Why do component names start with a capital letter?
In JSX, tags that start with a lowercase letter are interpreted as HTML elements, such as <div>, <button>. On the other hand, tags that start with an uppercase letter are interpreted as components defined in JavaScript. Therefore, components you create yourself must have names that start with an uppercase letter.
function Greeting() {
return <p>반갑습니다.</p>;
}
function App() {
return (
<main>
<Greeting />
</main>
);
}
<Greeting />means including the Greeting component on the screen. If you write it in lowercase letters like <greeting />, it will be processed as an HTML tag rather than a React component.
2.2.3 Components are used like tags in JSX
Components are used in the form of JSX tags such as <Greeting />rather than called directly like regular functions. JSX translates this notation into elements that React can interpret, and React renders those elements to the screen.
function Greeting() {
return <p>안녕하세요!</p>;
}
function App() {
return (
<section>
<h1>인사말</h1>
<Greeting />
<Greeting />
</section>
);
}
You can deploy the same component multiple times, and you can combine other components within the JSX returned by the component. A closing slash notation like <Greeting />is a JSX syntax used for components without content.
If you call the component directly like Greeting(), React will skip the flow of managing the component, so you should use it as a JSX tag. The component is placed in the form <Greeting /> for React to render.
2.2.4 exportand import
If you want to use the component in another file, type exportin that file and importin the file you want to use. If it is a component that is only used within the same file, it is not necessary to export it.
// Greeting.tsx
export default function Greeting() {
return <p>안녕하세요!</p>;
}
// App.tsx
import Greeting from './Greeting';
function App() {
return <Greeting />;
}
export default App;
export defaultspecifies the value to export by default from the file, and import Greeting from './Greeting'imports that value. exportis a JavaScript module syntax that allows components to be used in other files, but is not a necessary condition for becoming a component.
2.2.5 Return value of function component
The component returns JSX to display on the screen. To return multiple elements, wrap them in one parent element or as a Fragment with the <>...</> syntax.
function Profile() {
return (
<>
<h2>홍길동</h2>
<p>React 학습 중</p>
</>
);
}
A component may also return nullwhen it does not display anything depending on the condition. Understanding it as a function returning JSX is enough to get started.
2.2.6 Function components and class components
React components can be defined as functions or classes. In the past, class components were responsible for state and life cycle functions, but now it is common to use function components and Hooks. This course explains based on function components.
2.2.7 JSX grammar and rules - Expand / Collapse
JSX (JavaScript XML) is a JavaScript extension grammar that allows UI structures to be expressed in a syntax similar to HTML within JavaScript code. It was introduced by Facebook (now Meta), which created React, and JSX itself is not HTML or JavaScript that browsers understand. Build tools like Vite convert JSX into JavaScript that the browser can execute.
const title = 'React';
function Welcome() {
return <h1>안녕하세요, {title}!</h1>;
}
To use a JavaScript expression within JSX, surround it with curly braces {}. You can enter variables, function calls, arithmetic expressions, etc., but you cannot directly insert statements like the if statement or for statement. To display different content depending on conditions, you can use the ternary operator or &&.
function Greeting({ isLoggedIn }: { isLoggedIn: boolean }) {
return <p>{isLoggedIn ? '환영합니다!' : '로그인해 주세요.'}</p>;
}
When writing JSX, follow these rules:
A JSX element that returns - is wrapped by a single parent element. If you don't want to add an actual DOM element, you can use Fragment(<>...</>).
- All tags must be closed. Tags without content are written as <img />, <input />.
Use a JSX property name that is different from the ZXQXPREFIXQXZHTML property name. for exampleclassinsteadclassName, forinsteadhtmlForUse .
ZXQXPREFIXQXZJSX with multiple elementsreturnIt is recommended to enclose it in parentheses. Although it is not a required rule, it prevents errors that may occur due to JavaScript's automatic insertion of semicolons when changing line breaks or indentations, and makes the structure easier to read.
function Profile() {
return (
<section className="profile">
<h2>홍길동</h2>
<label htmlFor="email">이메일</label>
<input id="email" />
</section>
);
}
2.2.7.1 JSX conversion and React.createElement
JSX doesn't run directly in the browser, so build tools convert it to JavaScript. For example, past JSX conversion methods turned the following JSX into code similar to the React.createElement call:
const element = <h1 className="title">안녕하세요</h1>;
const element = React.createElement(
'h1',
{ className: 'title' },
'안녕하세요',
);
This is an example to understand the transformation principle. Modern React projects often use automatic JSX conversion and use the functions of react/jsx-runtimeinternally, so there is no need to manually write React.createElementor import Reactin the source code every time because of JSX.
If you want to check the JSX conversion result, you can try entering the JSX code into Babel's online converter. In real projects, Vite automatically converts them during the build process.
2.2.7.2 Beware of JSX escaping and HTML insertion
If you put a string or value in a text position in JSX, React will default to displaying it as plain text rather than interpreting it as HTML. This escapes HTML special characters, which helps protect user input from being executed with tags like <script> .
const userInput = '<script>alert("hello")</script>';
function Message() {
return <p>{userInput}</p>;
}
The above code displays the string as letters rather than executing it as a tag. For special cases where you need to insert an HTML string as an actual element, you can use dangerouslySetInnerHTML.
function Article() {
const html = '<strong>중요한 안내</strong>';
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
dangerouslySetInnerHTMLis an object of type { __html: 문자열 } . Inserting untrusted input as is may result in script injection (XSS) vulnerabilities, so even when necessary, input is verified and purified before use, and JSX text rendering is used for general screen output.
2.2.7.3 Writing comments in JSX
In JavaScript areas outside of JSX, use regular JavaScript comments, and when inserting comments between JSX elements, write comments within curly braces.
function Notice() {
// Comments in area JavaScript
return (
<section>
{/* Comments in JSX */}
<p>공지사항입니다.</p>
</section>
);
}
2.2.8 Component separation and combination
When the screen gets bigger, it is easier to read and manage by dividing it into smaller components with clear roles rather than writing all the JSX in one component. Dividing the UI in this way is called component separation. Separated components can be placed like tags in the JSX of the parent component to form a single screen, and this is called component combination.
type GreetingCardProps = {
name: string;
};
function PageHeader() {
return <header><h1>React 학습 노트</h1></header>;
}
function GreetingCard({ name }: GreetingCardProps) {
return (
<section>
<h2>환영합니다, {name}님!</h2>
<p>오늘도 React를 함께 배워 봅시다.</p>
</section>
);
}
function PageFooter() {
return <footer>학습을 응원합니다.</footer>;
}
function App() {
return (
<main>
<PageHeader />
<GreetingCard name="민지" />
<PageFooter />
</main>
);
}
Appis responsible for the large structure of the page,PageHeader, GreetingCard, PageFooterrepresents the UI each person is responsible for.GreetingCardIsnameYou can receive it as props and reuse it with a different name. In this way, the parent component combines the child components and passes the necessary data through props. For detailed usage of props,Chapter 3 “Component Development”It is covered in
There is no need to break down all the elements from the beginning. If a part of the screen has an independent role, is used repeatedly, or is convenient to read and modify separately, it is recommended to separate it into components. Too many small components can make it difficult to understand the overall structure, so choose an appropriate size based on role and reusability.
2.3 Condition, branch, repetition processing
2.3.1 Conditional Rendering
Conditional rendering is a method of selecting which JSX to display on the screen based on the results of a condition. JavaScript's if statement can be used outside of JSX, and inside JSX, the ternary operator or &&is often used.
type GreetingProps = {
isLoggedIn: boolean;
};
function Greeting({ isLoggedIn }: GreetingProps) {
if (!isLoggedIn) {
return <p>로그인해 주세요.</p>;
}
return <p>환영합니다!</p>;
}
When selecting between two results within JSX, you can use the ternary operator.
function LoginStatus({ isLoggedIn }: GreetingProps) {
return <p>{isLoggedIn ? '로그인 상태' : '로그아웃 상태'}</p>;
}
function Inbox({ unreadCount }: { unreadCount: number }) {
return (
<section>
<h2>받은 편지함</h2>
{unreadCount > 0 && <p>읽지 않은 메시지: {unreadCount}</p>}
</section>
);
}
Which method to use depends on how complex the condition is and how much content is to be rendered. Complex conditions are easier to read if calculated outside of JSX.
2.3.2 List rendering
When displaying data in an array repeatedly on the screen, each data is usually converted to a JSX element using map(). You can also conditionally handle screens when the list is empty.
type Course = {
id: number;
title: string;
};
const courses: Course[] = [
{ id: 1, title: '컴포넌트 기초' },
{ id: 2, title: '조건부 렌더링' },
];
function CourseList() {
if (courses.length === 0) {
return <p>강좌가 없습니다.</p>;
}
return (
<ul>
{courses.map((course) => (
<li key={course.id}>{course.title}</li>
))}
</ul>
);
}
filter()and display it as map(). sort()reorders the array itself, so it sorts the copy so that it does not directly sort the state or the original shared array.
const visibleCourses = courses
.filter((course) => course.title.includes('렌더링'))
.sort((a, b) => a.title.localeCompare(b.title));
In the example above, filter()creates a new array, and the subsequent call sort()sorts that new array, leaving the original courses array unchanged. Avoid calling courses.sort(...)directly on the original array.
map()If each item in the result consists of multiple JSX elements, you can group them into a Fragment. At this time, the list items are separated bykeyTo specify , the abbreviated grammar<>...</>insteadFragmentUse explicitly.
import { Fragment } from 'react';
function CourseDetails() {
return courses.map((course) => (
<Fragment key={course.id}>
<h3>{course.title}</h3>
<hr />
</Fragment>
));
}
2.3.2.1 List rendering key
keyis a special value that React uses to distinguish each item in the list from each other and to determine which elements have changed when items are added, deleted, or moved. When rendering a listmap()On the top element this returns:keymust be specified.
Whenever possible, use unique, stable identifiers embedded in your data (such as database IDs). An array index is not an appropriate key in a list where the order of items may change or intermediate items may be added or deleted. In this case, React may incorrectly link the old and new items, leaving inputs or component state in the wrong item. Consider using an index as a key only when the list is fixed and the order and items do not change.
{courses.map((course) => (
<li key={course.id}>{course.title}</li>
))}
Keys within the same list must be different, and a new random value must not be created and used as a key each time rendering. keyis a value for React to manage the list, so it is not passed as a general prop to the component. If the ID is needed within the component, it is passed separately from key.
3. Component Development - Expand / Collapse
3.1 Re-rendering timing
Re-rendering is the process by which React runs the component again to calculate new content to display on the screen. It usually starts when:
1.If the state of the component changes
When you update the value with the state setting function, React re-renders the component. Rendering may be skipped if it is determined that the new value is the same as the old value.
2.If the parent component has been re-rendered
When the parent is re-rendered, the children are also re-rendered by default. At this time, the child receives the props passed by the parent and recalculates the screen. memoallows you to skip rendering of children when the props are the same, but can be re-rendered for other reasons such as state or context changes.
3.If the context value in use has changed
useContext()are re-rendered when the value of the Provider that provides the value changes. How to use Context and useContext()is explained in Section 6.4 “useContext”.
Changing props is not a separate startup cause from parent rendering, but rather a situation where the values passed to the child change when the parent is re-rendered.It's easy to understand. For example, if the parent runs again and the name it passes to the child changes, the child recalculates the screen using the new name.
Re-rendering does not mean recreating the entire screen from scratch. React compares the component's newly calculated results with the previous results and reflects only the actual changes in the browser DOM. In other words, component being re-executed,, and , all elements of the screen being recreated,, are two different things.
3.2 props- Expand / Collapse
props(properties) is an object containing values that the parent component passes to the child component. In JSX<Greeting name="민지" />If you write it like this:nameis the name of the prop,"민지"is the value passed to that prop. React uses this value{ name: '민지' }as an object in the formGreetingPass it on.
type GreetingProps = {
name: string;
};
function Greeting(props: GreetingProps) {
return <p>안녕하세요, {props.name}님!</p>;
}
function App() {
return <Greeting name="민지" />;
}
In this example, GreetingPropsis just a TypeScript type name that checks the shape of the object being passed, not the value itself. propsis the object received by the component, and props.nameis the name value taken out from that object. Children do not change props directly, and value changes are handled by updating the state managed by the parent and passing new props. In the following sections, we look at props' data types, destructuring, default values, etc.
Data type of #### 3.2.1 props
In props, you can pass a variety of JavaScript values, including strings, numbers, booleans, arrays, objects, and functions.
<UserCard
name="민지"
age={20}
isActive={true}
hobbies={['독서', '산책']}
user={{ id: 1, role: 'admin' }}
/>
String props can be quoted, like name="민지". Use curly braces when passing JavaScript expressions, such as numbers, booleans, arrays, objects, variables, and functions.
3.2.2 props destructuring and default values
Destructuring assignment is a JavaScript grammar that takes the value of a specified property from an object and stores it in a variable with the same name. If you use it in a component parameter, you can immediately retrieve the required value after receiving the props object.
type GreetingProps = {
name: string;
};
// It takes a props object as a parameter and destructures the name within it.
function Greeting({ name }: GreetingProps) {
return <h2>안녕하세요, {name}님!</h2>;
}
function App() {
// In JSX, pass the name prop to the Greeting component.
return <Greeting name="민지" />;
}
Appsilver<Greeting name="민지" />asnamePass the prop. ReactGreetingThe object passed to is{ name: '민지' }It looks like this. of parameters{ name }In this objectnameTake out the property and local variablenameSince you put it in, inside the functionprops.nameinsteadnameYou can use it as: If you don't use destructuringfunction Greeting(props: GreetingProps)After receiving the object withprops.nameYou can access .
The default value to use when a prop is omitted can also be specified in the destructuring assignment of the parameter.
type GreetingProps = {
name?: string;
};
function Greeting({ name = '손님' }: GreetingProps) {
return <h2>안녕하세요, {name}님!</h2>;
}
function App() {
return <Greeting />;
}
In this example, <Greeting />does not pass name, so namedefaults to '손님'. The default value applies when the prop is omitted or is undefined. Since React does not set common default values for all props, you specify the values required for your component directly.
3.2.3 Function props and event processing
A parent component can pass a function as props to have the child component execute the parent's actions. When passing an event processing function, usuallyonClick, onChange, onSavelikeonUse a name that starts with .
type ActionButtonProps = {
onAction: () => void;
};
function ActionButton({ onAction }: ActionButtonProps) {
return <button onClick={onAction}>실행</button>;
}
function App() {
function handleAction() {
console.log('버튼을 눌렀습니다.');
}
return <ActionButton onAction={handleAction} />;
}
onClick={handleAction}passes a function to be executed when clicked. Be careful because if you add parentheses like onClick={handleAction()}, the function will be called directly during rendering. Types and propagation of React events, and cancellation of basic actions are explained in detail in Chapter 4 “Event Handling”.
3.2.4 children
In JSX<Card>The part that encloses only the component name, like thisopening tag, </Card>in front of the name like/The part that closes by addingclosing tagIt is said. The content inserted between the two tags is a special prop.childrenis passed to the component. This is useful when creating a UI, like a card, where the outer shape is the same but only the content inside is different.
import type { ReactNode } from 'react';
type CardProps = {
// ReactNode is a type that represents content that React can render.
children: ReactNode;
};
// The content received through the children prop is displayed in the card.
function Card({ children }: CardProps) {
return <section className="card">{children}</section>;
}
function App() {
// <Card>and </Card> are passed as children.
return (
<Card>
<h2>공지사항</h2>
<p>새로운 소식이 있습니다.</p>
</Card>
);
}
In this example, <Card>is the opening tag and </Card>is the closing tag. Because the title and paragraph were written between the two tags, this content is delivered as children. The Card component puts {children}inside <section> on the card and displays it on the screen. So, while the outer shape of the card can be reused, the content inside can be configured differently depending on where it is used.
3.3 state default useState- Expand / Collapse
state is a value that the component remembers between renderings. If you manage the values that affect the screen as state, then when the value is updated, React recalculates the screen with the new value. In the function component, create state and update functions with useState Hook.
import { useState } from 'react';
function Counter() {
// 0 is the initial state to use for the first render.
const [count, setCount] = useState(0);
function handleClick() {
// Request the next state value as a setter.
setCount(count + 1);
}
return <button onClick={handleClick}>클릭한 횟수: {count}</button>;
}
useState(0)returns a value in the form [현재 state, state setter] . By decomposing the array structure, the first value is stored in countand the second value is stored in setCount. 0is the initial value to be used when first rendering. When you press the button, setCount(count + 1)will request the next value, and React will re-render the component with the updated count. The state variable is not a function but contains the value of the current rendering and is updated with a setter rather than being directly assigned. The setter name is usually set followed by the state name (e.g. countand setCount).
The state is managed by the closest component that uses that value. If multiple children need to share the same state, the state can be raised to a common parent, and when a child requests a change, the parent can pass the function as props. This pattern is covered in 3.2.3 Function props and event processing.
3.3.1 Functional update
When changing the state, you can pass the new value directly to the setter, or pass a function that receives the previous state and returns the next value. Use functional updates to calculate new values based on previous values.
function Counter() {
const [count, setCount] = useState(0);
function increaseFromCurrentRender() {
// All three times it is calculated based on the same count of the current rendering.
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
function increaseWithUpdater() {
// Each function inherits and calculates the result of the previous update.
setCount((current) => current + 1);
setCount((current) => current + 1);
setCount((current) => current + 1);
}
return (
<>
<p>현재 값: {count}</p>
<button onClick={increaseFromCurrentRender}>현재 값으로 세 번 증가</button>
<button onClick={increaseWithUpdater}>함수형으로 세 번 증가</button>
</>
);
}
When the initial value is 0, pressing each button once operates as follows.
| Method | Value calculated by three calls | Final value left on screen |
|---|---|---|
setCount(count + 1) |
1, 1, 1 |
1 |
setCount(current => current + 1) |
1, 2, 3 |
3 |
Passing the value directly means that all three calls have the same rendering effect.countSince you use1request. In a functional update, the next function inherits the result of the previous update.1, 2, 3are calculated in turn. These numbers are not displayed separately on the screen, and the final value displayed on the screen after pressing the button once is1class3no see. The example below has two buttons with the samecountSince , when comparing results, each method is set to the initial value0It must be run separately from .
Let's also compare the results when the button using the same method is pressed twice in succession with the initial value of 0. The results of both methods were run separately.
| Method | First click | Second click | Final value after two clicks |
|---|---|---|---|
setCount(count + 1) |
1, 1, 1By requesting1 |
2, 2, 2by requesting2 |
2 |
setCount(current => current + 1) |
1 → 2 → 3 |
4 → 5 → 6 |
6 |
If you do not need the previous state, such as changing the value to a specific number, you can pass a value like setCount(10). The setCount(current => ...) format is suitable for calculations that depend on previous values and for continuous updates within one event.
3.3.2 State immutability
When using an object or array as the state, do not directly modify the existing value, but create a new object or array containing the changed contents and pass it to the setter. This is said to protect the immutability of the state.
type Profile = {
name: string;
city: string;
};
function ProfileCard() {
const [profile, setProfile] = useState<Profile>({ name: '민지', city: '서울' });
function changeCity() {
// Creates a new object containing the values to be changed, without modifying the existing object.
setProfile((currentProfile) => ({ ...currentProfile, city: '부산' }));
}
return <button onClick={changeCity}>{profile.name}: {profile.city}</button>;
}
{ ...currentProfile, city: '부산' }creates a new object that copies the existing properties and only changes city. Conversely, if you directly modify an existing object like profile.city = '부산'and pass the same object again, React may not properly detect the change. Arrays follow the same principle as passing a new array.
setItems((currentItems) => [...currentItems, newItem]);
setItems((currentItems) => currentItems.filter((item) => item.id !== targetId));
The first example copies existing items and adds new ones, while the second example creates a new array containing only the items that match the conditions. Even when changing an object within an object, you must copy the objects in the change path to create a new value.
4. Event Handling - Expand / Collapse
4.1 React events and handlers
An event is a user action in the browser, such as a click, typing, or keyboard operation. In React, you specify event names such as onClickand onChangein JSX elements and process them by passing a function to execute. Event names are written in camel case, and functions are passed without being called.
| Event prop | Occurring situation |
|---|---|
onClick |
When clicking on element |
onChange |
When the input value changes |
onSubmit |
When submitting form |
onKeyDown |
|
onFocus, onBlur |
When an element gains or loses focus |
function SaveButton() {
function handleClick() {
console.log('저장 버튼을 눌렀습니다.');
}
// Pass a function to be executed to onClick.
return <button onClick={handleClick}>저장</button>;
}
onClick={handleClick}passes a function to run when clicked. Be careful because if you add parentheses like onClick={handleClick()}, it will be called immediately during rendering. Event information that has occurred is delivered to the event handler, and input values, etc. can be read using this.
4.2 Event propagation
If the element outside the element where the event occurred also has the same event handler, the event can be propagated to the parent element. This is called event propagation. In nested elements, this behavior may cause the parent's handler to also be executed on a child click.
4.2.1 Capture and Bubble Phase
The event goes through the capture stage, which goes down from the outer element to the inner element, and then goes through the bubble stage,, which goes up outward from the actual event target. React's onClickruns by default in the bubble phase, and onClickCaptureruns in the capture phase.
function ClickOrder() {
return (
<div
onClickCapture={() => console.log('부모 캡처')}
onClick={() => console.log('부모 버블')}
>
<button
onClickCapture={() => console.log('자식 캡처')}
onClick={() => console.log('자식 버블')}
>
클릭
</button>
</div>
);
}
When you click the button, it runs in the order 부모 캡처 → 자식 캡처 → 자식 버블 → 부모 버블 . onClickCaptureis used when there is logic that must be processed first during the capture stage.
Stop propagation with #### 4.2.2 stopPropagation
If you don't want the event to propagate all the way to the parent handler, call event.stopPropagation()in the handler.
function NestedClick() {
return (
<div onClick={() => console.log('부모 클릭 핸들러')}>
<button
onClick={(event) => {
// Prevents click events from propagating to parent elements.
event.stopPropagation();
console.log('자식 클릭 핸들러');
}}
>
부모 동작 없이 클릭
</button>
</div>
);
}
When I press the button, only the child handler is executed and the parent handler is not executed. stopPropagation()only prevents event delivery, but does not cancel basic browser actions such as moving links.
4.3 Cancel default action with preventDefault
Use event.preventDefault()to prevent actions that the browser performs by default, such as moving a link or submitting a form.
function DetailLink() {
return (
<a
href="/detail"
onClick={(event) => {
// Cancels the default movement behavior of a link.
event.preventDefault();
console.log('이동 대신 다른 처리를 합니다.');
}}
>
상세 보기
</a>
);
}
Clicking this link does not take you to /detail, it only runs the handler. preventDefault()prevents the browser default behavior, and stopPropagation()prevents events from propagating to the parent. The two have different purposes and can be used together when necessary.
5. Form - Expand/Collapse
A form is a UI that collects, verifies, and submits the values entered by the user. In React, you can use controlled components that manage input values in the state and uncontrolled components that leave input values to the DOM. Which method to choose depends on whether the input value needs to be used immediately on the screen or whether a form library is used.
5.1 Form input management- Expand / Collapse
5.1.1 What is the difference between target and current?
event.targetis the element where the event actually started, and event.currentTargetis the element to which the current handler is attached. When you attach a handler directly to an input element, both usually point to that input element. In forms, it is easier to understand the type and behavior by using currentTarget, which clearly indicates the input element to which the handler is attached.
| Classification | Controlled component | Non-controlled component |
|---|---|---|
| Input value storage | React state | DOM input element |
| Read input value | event.currentTarget.value , etc. |
When submitting ref's current.value |
| Default | value or checked |
defaultValue or defaultChecked |
| Reflected on screen during input | Re-rendered when state changes | Input itself does not cause state update |
5.1.2 Control components
Control componentconnects input values to React state. Since the state is updated whenever the input value changes, it is suitable for immediately changing or verifying the screen according to the input value.
import { useState } from 'react';
function ControlledName() {
const [name, setName] = useState('');
return (
<label>
이름
{/* The value is controlled by the state, and onChange reflects the new input value to the state. */}
<input
value={name}
onChange={(event) => setName(event.currentTarget.value)}
/>
<p>입력 중: {name}</p>
</label>
);
}
value={name}displays the current state in the input element, and onChangestores the value entered by the user in the state as setName. Therefore, the state and the value of the input window are updated together.
5.1.3 Uncontrolled component
The uncontrolled componentstores the input value in the DOM. Rather than updating the React state every time something is input, the value is read through ref when needed.
import { useRef } from 'react';
import type { FormEvent } from 'react';
function UncontrolledName() {
// ref points to the actual <input> element, which is initially null as it is not yet connected.
const nameRef = useRef<HTMLInputElement>(null);
// The element where the submission event occurred is <form>.
function handleSubmit(event: FormEvent<HTMLFormElement>) {
// Prevents the browser from doing the default action of submitting the form and reopening the page.
event.preventDefault();
console.log(nameRef.current?.value ?? '');
}
return (
<form onSubmit={handleSubmit}>
<label>
이름
{/* defaultValue is the initial input value, and the type of ref is HTMLInputElement. */}
<input ref={nameRef} defaultValue="민지" />
</label>
<button type="submit">확인</button>
</form>
);
}
defaultValueis the value to be displayed initially, and after modification by the user, the input value in the DOM becomes the standard. You can use defaultCheckedfor the checkbox. This means that React does not control input values through state, but it does not prevent the component from being rendered for other reasons.
5.2 Example of form implementation according to input elements- Expand / Collapse
5.2.1 Managing input elements by state
When creating text input, multi-line input, selection box, and checkbox as control components,valueorcheckedandonChangeConnect . The value of the checkbox isvaluenotcheckedRead from
import { useState } from 'react';
import type { FormEvent } from 'react';
type ContactFormState = {
name: string;
message: string;
topic: string;
agreed: boolean;
};
function ContactForm() {
const [form, setForm] = useState<ContactFormState>({
name: '',
message: '',
topic: 'question',
agreed: false,
});
function handleSubmit(event: FormEvent<HTMLFormElement>) {
// Prevents the browser's default behavior of refreshing the page after submission.
event.preventDefault();
console.log(form);
}
return (
<form onSubmit={handleSubmit}>
<label>
이름
<input
value={form.name}
onChange={(event) =>
setForm((current) => ({ ...current, name: event.currentTarget.value }))
}
/>
</label>
<label>
문의 내용
<textarea
value={form.message}
onChange={(event) =>
setForm((current) => ({ ...current, message: event.currentTarget.value }))
}
/>
</label>
<label>
문의 종류
<select
value={form.topic}
onChange={(event) =>
setForm((current) => ({ ...current, topic: event.currentTarget.value }))
}
>
<option value="question">질문</option>
<option value="feedback">의견</option>
</select>
</label>
<label>
<input
type="checkbox"
checked={form.agreed}
onChange={(event) =>
setForm((current) => ({ ...current, agreed: event.currentTarget.checked }))
}
/>
개인정보 수집에 동의합니다.
</label>
<button type="submit">보내기</button>
</form>
);
}
Each input element associates the corresponding field in the state to value or checked, and updates it by creating a new state object when it changes. When submitting a form, handleSubmitcan use the current state. In an actual service, required inputs and formats must be verified before submission, and the success or failure of the server request must also be notified to the user.
5.2.2 Update nested object state
Even if there is an object or array in the form state, create a new object or array rather than directly changing the existing value. For example, when changing one field of a nested object, each object in the changing path is copied.
setForm((current) => ({
...current,
address: {
...current.address,
city: '부산',
},
}));
5.2.3 Array state update
When React checks for state changes, it also compares references to the old and new values. If you modify the array directly, you will keep the same array object, so the change may not be detected and the screen may not be updated. Therefore, when adding, modifying, or deleting array items, instead of replacing the original array push() , a new array is created and passed to the state. The example below assumes that items, setItems, and each of id, newItem, updatedItem, and targetIdare already defined.
// Append: Creates an array containing existing and new items.
setItems((current) => [...current, newItem]);
// Edit: Replace only the target item with a new object.
setItems((current) =>
current.map((item) => (item.id === updatedItem.id ? updatedItem : item)),
);
// Delete: Create a new array containing only the items you want to keep.
setItems((current) => current.filter((item) => item.id !== targetId));
5.2.4 Update nested state with Immer
When your code becomes complex with multiple levels of copying of nested objects, you can use Immer.use-immerof the packageuseImmercreates a new changed state even if you write it as if you were directly editing the draft.
import { useImmer } from 'use-immer';
type Profile = {
name: string;
address: { city: string };
};
function ProfileEditor() {
const [profile, updateProfile] = useImmer<Profile>({
name: '민지',
address: { city: '서울' },
});
function changeCity() {
// A draft is a temporary work object that Immer provides to the updater.
// Any modifications made here will be reflected by Immer as a new immutable state.
updateProfile((draft) => {
draft.address.city = '부산';
});
}
return <button onClick={changeCity}>{profile.address.city}</button>;
}
Since Immer's draft is not a general state object, this method is only used within the updater of useImmer. Objects or arrays of the base useStateshould not be modified directly.
5.3 React Hook Form- Expand / Collapse
Install #### 5.3.1
React Hook Form is a library that helps register input values, submit forms, and verify. You can reduce the need to repeatedly write state and change handlers for each field in forms with many inputs.
Install the package to use it in your Vite project.
npm install react-hook-form
5.3.2 basic usage
import { useForm } from 'react-hook-form';
type SignupValues = {
email: string;
password: string;
};
function SignupForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<SignupValues>({
defaultValues: { email: '', password: '' },
});
function onSubmit(values: SignupValues) {
console.log(values);
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>
이메일
<input
{...register('email', { required: '이메일을 입력해 주세요.' })}
type="email"
/>
</label>
{errors.email && <p role="alert">{errors.email.message}</p>}
<label>
비밀번호
<input
{...register('password', {
required: '비밀번호를 입력해 주세요.',
minLength: { value: 8, message: '8자 이상 입력해 주세요.' },
})}
type="password"
/>
</label>
{errors.password && <p role="alert">{errors.password.message}</p>}
<button type="submit">가입</button>
</form>
);
}
register()registers each input in the React Hook Form, and handleSubmit()is verified upon submission and passes the successful value to onSubmit. This process also handles navigating the form's main page, so we don't call preventDefault()separately in the example. errorscontains verification failure information. Password length verification is for user convenience; actual security verification must also be performed on the server.
6. Hook utilization - Expand / Collapse
6.1 useState: state and setters
useStatecreates a value to persist between renderings, and returns [현재 값, 갱신 함수]. For detailed functional updates and invariants, please refer to Section 3.3.
const [count, setCount] = useState(0);
Hooks are called at the top of a component or user-defined hook, and are not called within conditional or loop statements.
6.2 useEffect: synchronize with external system
useEffectis used to synchronize systems and components outside of React, such as timers, event subscriptions, and network connections.
useEffect(() => {
// Tasks to synchronize with external systems
}, [의존성]);
The first argument is the function to be executed, and the second argument is an array (dependency ship) of responsive values such as props and state used by the function.
If you omit the dependencies array, it will run every time rendering is committed,
[], it runs after it first appears on the screen and does not run again during normal re-rendering.
[value], it will run for the first time and then run again every time the value changes.
The function returned by Effect is the theorem function ****. Cleanup means finishing or unwinding the work started by the Effect. For example, if you started a timer, the cleanup function stops the timer.
useEffect(() => {
const timer = setInterval(() => setCount((current) => current + 1), 1000);
return () => clearInterval(timer);
}, []);
React calls the previous cleanup function before running the Effect again, and also calls it when the component is removed from the screen. In the development environment Strict Mode, additional initial setup and cleanup may be run to verify cleanup logic.
import { useEffect, useState } from 'react';
function PageTitle({ title }: { title: string }) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `${title}: ${count}`;
}, [title, count]);
return <button onClick={() => setCount((current) => current + 1)}>{count}</button>;
}
documentis a DOM API provided by the browser and is a system outside of React components. This example synchronizes the browser tab title (document.title) with React's title prop and count state. We put the two values in the dependency array, so if either of them changes after the first run, the Effect will run again.
6.3 useRef: value reference separate from rendering
useRefreturns a { current } object that persists between renderings. Used to reference DOM elements or to store values that are not displayed on the screen, such as timer IDs. Since currentis not re-rendered even if changed, the value shown on the screen is managed by state. For input ref examples, see Section 5.1.3.
6.4 useContext: Passing values to deep components
Context helps avoid having to keep passing required values as props several levels down. useContextreads the value of the nearest parent Provider, and if that value changes, the component that uses it is re-rendered.
import { createContext, useContext } from 'react';
const ThemeContext = createContext('light');
function Toolbar() {
const theme = useContext(ThemeContext);
return <div className={theme}>도구 모음</div>;
}
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
Toolbarreads the "dark" value provided by the parent provider and renders <div className="dark">. Context only passes values and does not apply styles to the class. To display dark background and light text, you need to define CSS rules.
.dark {
background-color: #222;
color: white;
}
Context is used for values to be shared at various depths, such as themes or logged in users, and does not unconditionally move all states.
6.5 useReducer: Complex state update
useReducergathers several related state change rules into one reducer function and updates them with dispatch(action). Useful when state transitions are complex or multiple events change the same state; useStateis simpler for simple values.
6.6 Performance Optimization Hook
useMemoreuses the calculation result, and useCallbackreuses the function until the dependency value changes. Use only when necessary, such as for calculations with known performance issues or for passing values to optimized child components. Unnecessary memoization can complicate your code and reduce manual use in projects using the React Compiler.
6.7 useId: Create accessibility ID
useIdis used to create a unique ID to associate input elements and descriptions with htmlFor, id, and aria-describedby. This is not a Hook to create keyin the list.
6.8 Prioritized UI updates
useTransitionand useDeferredValueslow down heavy screen updates such as search results or large lists, allowing urgent UI such as input to respond first. This is a different function from debounce, which delays network requests for a certain period of time.
6.9 Action Hook in React 19
In React 19 and higher, you can manage the result and progress of the action with useActionState, and display optimistic UI before the request ends with useOptimistic. Check the React version of your project and use it if you need the function.
7. Embedded Component - Expand / Collapse
7.1 Fragment
Fragments bundle multiple JSX elements together without adding unnecessary parent elements to the actual DOM. The short notation is <>...</>, and if you need key, you explicitly get and use Fragment.
import { Fragment } from 'react';
function CourseDetails() {
return (
<>
<h2>React 기초</h2>
<p>컴포넌트와 JSX를 배웁니다.</p>
</>
);
}
function CourseRows({ courses }: { courses: { id: string; title: string }[] }) {
return courses.map((course) => (
<Fragment key={course.id}>
<h3>{course.title}</h3>
<hr />
</Fragment>
));
}
keycannot be specified in abbreviated notation. For the keys in the list, refer to Section 2.3.2.1.
7.2 StrictMode
StrictModeis a React component that performs additional checks to find potential problems in the component during development. This does not affect the behavior or display of production builds. In a development environment, you can additionally render components to find impure rendering, and rerun relevant processing to find missing effects and ref cleanups.
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
During development, even if it looks like a log or Effect is running twice, rather than turning off checking, keep the rendering pure and make sure you write the Effect's cleanup function correctly. Strict Mode does not mean running the Effect twice in production.
7.3 Profiler
Profileris a React component that measures the rendering time of the wrapped component tree. It is used to measure areas where performance problems are suspected, and performance optimization is not applied without measurement.
import { Profiler } from 'react';
function CoursePage() {
return (
<Profiler
id="CourseList"
onRender={(id, phase, actualDuration) => {
console.log(`${id} ${phase}: ${actualDuration}ms`);
}}
>
<CourseList />
</Profiler>
);
}
onRenderis called when rendering of the wrapped tree is committed. actualDurationis the time it took to render the tree for that update. Since measurements have overhead and profiling is disabled in default production builds, you can also leverage the Profiler in React Developer Tools during development.
7.4 Suspense
Suspenseis a border that shows the fallback UI while the child component suspends rendering for loading. Wraps the scope where the loading UI will be displayed, and shows the child screen when it is ready.
import { Suspense } from 'react';
function App() {
return (
<Suspense fallback={<p role="status">화면을 불러오는 중입니다...</p>}>
<LazyCoursePage />
</Suspense>
);
}
Suspensedoes not automatically detect all asynchronous operations.lazyIt operates in framework/data loading methods that support components or suspension.useEffectinfetch()does not automatically enable Suspense, in this lesson we handle it directly as a state (Section 7.7). The loading fallback and error screen are also separate, and error handling consists of Error Boundary, etc.
7.5 Lazy loading components
lazy()defers component code to be loaded when it is first rendered. This is useful when dividing and loading code for large screens or functions that are not immediately needed on the initial screen. During lazy loading, a fallback to the nearest Suspense boundary is displayed.
import { lazy, Suspense } from 'react';
const SettingsPage = lazy(() => import('./SettingsPage'));
function App() {
return (
<Suspense fallback={<p role="status">설정 화면을 불러오는 중입니다...</p>}>
<SettingsPage />
</Suspense>
);
}
lazy() declaration outside the component. The module returned by dynamic import()must provide a React component with a default export (default export). Since Suspense does not handle errors in which a lazy-loaded module fails to load, a separate error UI is prepared using Error Boundary, etc.
7.6 Import data over network
Network requests are asynchronous operations. Requests for specific actions, such as a user click, can be initiated in an event handler, and requests to synchronize data required for the screen with an external system can be handled by Effect or the framework's data loading function. Effects are not added to retrieve values that can be calculated with props and state.
fetch()is rejected for network errors, but it is not rejected for 404 or 500 responses alone, so you must check response.ok.
async function fetchCourses() {
const response = await fetch('/api/courses');
if (!response.ok) {
throw new Error(`강좌 요청 실패 (${response.status})`);
}
return response.json();
}
Since the actual response data is not verified by TypeScript type declarations alone, you must also check the response shape at runtime. Examples including request cancellation, loading, errors, and displaying empty results are covered in the following section 7.7. As your app grows, consider a framework or data request library that provides caching, duplicate requests, and revalidation.
7.7 Loading/Error/Empty Result Handling
Screens that retrieve data from the network should distinguish between Requesting, Request Failed, No Results, and Data Displaying statuses. During loading, it notifies you that it is in progress, and displays a message to distinguish errors from empty results. It also provides retry functionality if needed. The example below assumes that /api/coursesreturns an array of courses of the form { id, title } .
import { useEffect, useState } from 'react';
type Course = { id: string; title: string };
type LoadState =
| { status: 'loading' }
| { status: 'error'; message: string }
| { status: 'success'; courses: Course[] };
function isCourseList(data: unknown): data is Course[] {
return (
Array.isArray(data) &&
data.every(
(item) =>
item !== null &&
typeof item === 'object' &&
'id' in item &&
typeof item.id === 'string' &&
'title' in item &&
typeof item.title === 'string',
)
);
}
function CourseList() {
const [state, setState] = useState<LoadState>({ status: 'loading' });
useEffect(() => {
const controller = new AbortController();
async function loadCourses() {
try {
const response = await fetch('/api/courses', { signal: controller.signal });
if (!response.ok) {
throw new Error(`강좌를 불러오지 못했습니다. (${response.status})`);
}
const data: unknown = await response.json();
if (!isCourseList(data)) {
throw new Error('서버 응답 형식이 올바르지 않습니다.');
}
setState({ status: 'success', courses: data });
} catch (error: unknown) {
if (controller.signal.aborted) return;
setState({
status: 'error',
message: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.',
});
}
}
void loadCourses();
return () => controller.abort();
}, []);
if (state.status === 'loading') return <p role="status">강좌를 불러오는 중입니다...</p>;
if (state.status === 'error') return <p role="alert">{state.message}</p>;
if (state.courses.length === 0) return <p>등록된 강좌가 없습니다.</p>;
return (
<ul>
{state.courses.map((course) => (
<li key={course.id}>{course.title}</li>
))}
</ul>
);
}
LoadStatewas defined as the discriminant union to have only the values required for each screen state. fetch()is rejected for network failures, but it is not rejected for HTTP responses such as 404·500, so check response.ok. The response JSON is received as unknownand its appearance is checked to prevent incorrect data from being used as normal results.
Effect's cleanup function cancels the request with AbortController. After the component disappears, the results are not reflected in the state, and a distinction is made between request failure and cancellation. Real-world services may require retries, caching, redundant request handling, etc., so consider data request library or framework features. Requests made directly inside Effect, like in this example, do not show Suspense's loading screen per se; they render the UI directly based on the state.
8. Style in component development - Expand / Collapse
Component styles can be written as separate CSS files, CSS Modules, CSS-in-JS libraries, etc. CSS-in-JS is a way to define styles in JavaScript/TypeScript code and manage them with components. Each library has different framework support, server rendering settings, bundle size, and usage syntax, so select one according to your project environment.
8.1 Styled JSX
Styled JSX is a CSS-in-JS tool primarily used by Next.js. If you write CSS at <style jsx>inside a component, it will be applied to the scope of that component. It is not included by default in regular Vite React projects, so separate settings and dependencies are required in environments other than Next.js.
function Greeting() {
return (
<section>
<h2>안녕하세요</h2>
<style jsx>{`
h2 {
color: navy;
}
`}</style>
</section>
);
}
It is convenient to separate the scope of component styles, but global styles require a separate global style syntax. When using Next.js App Router with server rendering, style registry settings may be required, so check the framework's official documentation.
8.2 styled-components
styled-componentsis a library that writes CSS with tagged template literals and creates React components with attached styles.
npm install styled-components
import styled from 'styled-components';
const Button = styled.button`
padding: 0.5rem 1rem;
border: 0;
border-radius: 0.25rem;
background: #2457a7;
color: white;
&:hover {
background: #173d78;
}
`;
function SaveButton() {
return <Button>저장</Button>;
}
Buttonapplies the linked styles while rendering the actual <button>. You can also change the style depending on the state and theme using props or ThemeProvider. Frameworks that use server rendering may require style extraction/injection setup, so follow the installation instructions for those frameworks.
8.3 Emotion
Emotion is a CSS-in-JS library that helps you write CSS in JavaScript. In React@emotion/styledYou can create components similar to styled-components,@emotion/reactofcssYou can also style elements with props. Here the setup is simple@emotion/styledmethod is used.
npm install @emotion/react @emotion/styled
import styled from '@emotion/styled';
const Message = styled.p`
margin: 0;
color: #444;
font-weight: 600;
`;
function Status() {
return <Message>저장되었습니다.</Message>;
}
Emotion provides several writing methods, including string templates, object styles, and the css prop. The css prop may require JSX conversion settings, so check your project's build tools and Emotion documentation. For environments that use server components and streaming, such as Next.js App Router, you must first check the support status and settings for each library.
All three methods allow you to configure styles on a component-by-component basis, but their syntax and framework integration methods are different. On a project, unless there's a good reason to, try to stay consistent with your existing approach rather than mixing up multiple styling tools.
9. React Router - Expand / Collapse
9.1 SPA
SPA (Single-Page Application) is a method in which JavaScript updates necessary parts of the screen while maintaining the initially received HTML document. React Router connects URLs and React components to switch screens without requesting a new HTML document each time, and supports browser back/forward movement and address sharing. The server must be set to return an app entry document even when accessing the app's path directly.
9.2 Deploying components without a router
Small screens can conditionally render components based on their state.
import { useState } from 'react';
function App() {
const [page, setPage] = useState<'home' | 'courses'>('home');
return (
<>
<button onClick={() => setPage('home')}>홈</button>
<button onClick={() => setPage('courses')}>강좌</button>
{page === 'home' ? <h1>홈</h1> : <h1>강좌 목록</h1>}
</>
);
}
With this method alone, the page state is not reflected in the URL, so you must manually implement address sharing or browser back. Apps that need to connect screens to routes use routers.
Deployment using ### 9.3 router
This example uses the declarative API of React Router v7. Install the package and add it to the top of the appBrowserRouterAfter putting ,RoutesConnecting the URL and screen insideRouteDeclare .
npm install react-router
// src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router';
import App from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
{/* Wraps the ability to use URL-based routing throughout the app. */}
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
);
// src/App.tsx
import { Route, Routes } from 'react-router';
import { Navigation } from './Navigation';
function App() {
// Select the route that matches the current URL.
return (
<>
{/* Common navigation is placed outside Routes, which change depending on the route. */}
<Navigation />
<Routes>
{/* Link the home screen to the home path. */}
<Route path="/" element={<Home />} />
{/* Connect the list screen to the course list path. */}
<Route path="/courses" element={<CourseList />} />
{/* :courseId is the path parameter to read from the URL. */}
<Route path="/courses/:courseId" element={<CourseDetail />} />
{/* This is the default path displayed when it does not match the path above. */}
<Route path="*" element={<p>페이지를 찾을 수 없습니다.</p>} />
</Routes>
</>
);
}
BrowserRouterassociates the browser address with the routing state. Routesfinds a path that matches the current URL and renders the matching Route's element. The * path is the screen to display when there is no matching path. Installation method and import path may vary depending on React Router version, so check the official documentation for your project version.
It is simple to change the screen directly using the state, but if it is not connected to the URL, sharing the screen address or moving the browser back/forward must be implemented separately. To open each screen with a unique URL and link it to your browser history, associate the URL and screen with Route.
9.4 path parameters
The part of the path that begins with a colon, such as :courseId, is a parameter. You can read the value with useParams()and display a detailed screen of the data.
import { useParams } from 'react-router';
const courses = [
{ id: 'react-basics', title: 'React 기초' },
{ id: 'react-hooks', title: 'React Hooks' },
];
function CourseDetail() {
const { courseId } = useParams();
const course = courses.find((item) => item.id === courseId);
if (!course) return <p>강좌를 찾을 수 없습니다.</p>;
return <h1>{course.title}</h1>;
}
route pattern/courses/:courseIdat:courseIdindicates the place where the actual value will be entered. actual url/courses/react-basicsThere is no colon inuseParams()IscourseIdby value"react-basics"returns . The path parameter is a string, so if it is a numeric ID, it is converted and validated before use.
Linkis a link that moves the route within the app. Clicking it replaces the URL and leaves the move to the router to avoid requesting the entire document again. It does not automatically provide the active style for the current path. For navigation that requires an active state, use NavLink.
import { Link } from 'react-router';
function Home() {
return <Link to="/courses">강좌 목록 보기</Link>;
}
Use <a>for general browser navigation, such as sites outside the app or downloaded files.
9.6 Show current location with NavLink
NavLinkIsLinkIt is a link that moves like this and also tells you if the current URL and path match. Easy to change style depending on active state, and for accessibility if matchingaria-current="page"is also set. When you clickNavLinkIt does not itself render page components. When the URL changesRoutesmatchesRouteofelementDisplays .
// src/Navigation.tsx
import { NavLink } from 'react-router';
export function Navigation() {
return (
<nav>
<NavLink to="/" end className={({ isActive }) => (isActive ? 'active' : '')}>
홈
</NavLink>
<NavLink to="/courses" className={({ isActive }) => (isActive ? 'active' : '')}>
강좌
</NavLink>
</nav>
);
}
isActiveis true, class active applies. Home Link's endcorrectly treats only the / path as active, preventing Home Link from being activated on other paths as well.
If you want your class to change color or appearance on the screen, you'll also need to define CSS rules.
Official document: React Router Declarative Routing.
10. Portal - Expand / Collapse
10.1 Separating DOM location with Portal
Portal renders the children of the React component tree in different locations in the DOM. This is useful when displaying overflowof a parent element, such as a modal or tooltip, without being obscured by the stacking context. Even when rendering with Portal, it retains the fact that it is a child of the React tree, so you can use the Context, and events are propagated along the React tree rather than the DOM location.
import { createPortal } from 'react-dom';
function Modal({ onClose }: { onClose: () => void }) {
return createPortal(
<div className="modal-backdrop" onClick={onClose}>
<section
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
onClick={(event) => event.stopPropagation()}
>
<h2 id="modal-title">알림</h2>
<p>Portal로 document.body에 렌더링한 대화상자입니다.</p>
<button onClick={onClose}>닫기</button>
</section>
</div>,
document.body,
);
}
createPortal(children, domNode)As the second argument of , the actual DOM node (document.bodyetc.). Clicking on the background causes it to close, and clicking inside the dialog box prevents propagation. The actual modal must also implement accessibility behaviors such as keyboard focus management and escape handling, which Portal itself does not provide automatically.
10.2 Handling rendering errors with Error Boundary
Error Boundary is a boundary that catches errors that occur during the rendering process of child components and displays an alternative UI instead of stopping the entire app. Currently, in React, Error Boundary is implemented as a class component, and the fallback state can be set to getDerivedStateFromErrorand the error can be recorded at componentDidCatch.
import { Component, type ErrorInfo, type PropsWithChildren } from 'react';
type ErrorBoundaryState = { hasError: boolean };
class ErrorBoundary extends Component<PropsWithChildren, ErrorBoundaryState> {
state: ErrorBoundaryState = { hasError: false };
static getDerivedStateFromError(): ErrorBoundaryState {
return { hasError: true };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error('화면 렌더링 오류:', error, info.componentStack);
}
render() {
if (this.state.hasError) {
return <p role="alert">화면을 표시하는 중 문제가 발생했습니다.</p>;
}
return this.props.children;
}
}
Wrap the subarea where you want the error to be isolated with <ErrorBoundary>. Placing boundaries in multiple places helps you break up the scope so that errors on one screen don't displace your entire app.
<ErrorBoundary>
<CoursePage />
</ErrorBoundary>
Error Boundary handles rendering errors in child components, but does not automatically catch errors raised by event handlers, errors in general asynchronous operations, or errors raised by the boundary itself. These errors are handled in a manner appropriate to where they occurred, such as try/catchor request status processing.
Reference: createPortal official document, Component official document.
11. Utilizing the React library - Expand / Collapse
The React ecosystem has libraries that help you quickly configure UI, develop components independently, and manage server data. Check and select the required features and React/build tool environment for your project.
11.1 UI composition with MUI Material
MUI Material is a React library that provides a theme system and reusable UI components such as buttons, inputs, menus, and dialog boxes. You can use it to quickly create screens with a consistent design in your project. MUI's default style engine is Emotion.
npm install @mui/material @emotion/react @emotion/styled
import Button from '@mui/material/Button';
function SaveButton() {
return (
<Button variant="contained" color="primary" onClick={() => console.log('저장')}>
저장
</Button>
);
}
variantselects the button appearance and colorselects the color role of the theme. MUI components provide accessibility properties and interactions, but screen-appropriate labels and usage flows must be configured in the app. React and React DOM are peer dependencies of MUI, so they must be installed in the project.
Official Documentation: MUI Material Installation, Button.
11.2 Developing components in Storybook
Storybook is a development tool that runs components separately from the rest of the app's screens and checks and documents the UI state according to various input values. Projects using React and Vite run the initialization command from the project root.
npx storybook@latest init
After initialization, Storybook runs the scripts you added to the project. The default setup starts the development server with npm run storybookand builds static output with npm run build-storybook.
Story files export basic information (meta) and individual states (story) of components. The following example assumes that the Button component receives labeland disabled props.
// src/components/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from './Button';
const meta = {
title: 'Components/Button',
component: Button,
args: { label: '저장' },
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: { disabled: false },
};
export const Disabled: Story = {
args: { disabled: true },
};
argsof each story is passed as component props. You can independently check the state of the component on the Storybook screen, and if Controls are set, you can check its behavior by changing props. Story files are usually not part of the actual app screen, but are used in development and documentation tools.
Official Documentation: StorybookGetting Started, Writing a Story.
11.3 Server status management with TanStack Query
TanStack Query is a server state management library, previously named React Query. Manages requests, caching, synchronization and updates of data retrieved from servers. It has a different purpose from useState, which manages client UI state such as input window value or open status.
npm install @tanstack/react-query
Wrapping your app with QueryClientProviderallows you to use the query function in child components.
// src/main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createRoot } from 'react-dom/client';
import App from './App';
const queryClient = new QueryClient();
createRoot(document.getElementById('root')!).render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>,
);
useQueryidentifies data as queryKey, and is imported as queryFn. In the example below, we distinguish between request, error, empty result, and success screens.
import { useQuery } from '@tanstack/react-query';
type Course = { id: string; title: string };
async function fetchCourses(): Promise<Course[]> {
const response = await fetch('/api/courses');
if (!response.ok) {
throw new Error(`강좌 요청 실패 (${response.status})`);
}
return response.json();
}
function CourseList() {
const { data, isPending, isError, error } = useQuery({
queryKey: ['courses'],
queryFn: fetchCourses,
});
if (isPending) return <p role="status">강좌를 불러오는 중입니다...</p>;
if (isError) return <p role="alert">{error.message}</p>;
if (data.length === 0) return <p>등록된 강좌가 없습니다.</p>;
return (
<ul>
{data.map((course) => (
<li key={course.id}>{course.title}</li>
))}
</ul>
);
}
Request results using the same queryKeycan be cached and reused by QueryClient. Since the shape of the data in the actual API response is not verified by TypeScript declarations alone, we also consider runtime verification. For detailed information on loading, error, and empty result handling and response verification, refer to Section 7.7.
Official document: TanStack Query Overview, Quick Start.
12. Test - Expand / Collapse
Testing is a way to ensure that your code behaves as required. From unit logic to actual browser user flow, we inspect it at a level that suits your purpose.
12.1 Types of tests
-Unit testing: Checks whether a small function or module produces the expected results. -Integration Test: Checks whether various parts such as components, states, and events work together. -E2E Test: Verify that the user's core flow works in a real browser.
React component testing can also be an integration test because it examines screens and user behavior. Test-level boundaries are not absolute.
12.2 Select test tool
-Jest or Vitest: A framework for running tests and validating the results. You can keep your existing project tools, or choose Vitest from your Vite project. -React Testing Library: A tool that renders components on the screen and helps check them from the user's perspective. -Playwright: An E2E testing tool that inspects user flows in real browsers. Cypress can also be used as an alternative. -Mock Service Worker (MSW): Helps test API success/error situations by mocking network responses.
Official documents: Jest, Vitest, React Testing Library, Playwright, MSW.
12.3 Choosing a test in practice
Unit and integration tests quickly check various operations, and E2E tests verify important user flows in the browser. Rather than examining every action with E2E, select the appropriate level based on importance and risk.
- tests requirements and user-verified results rather than internal implementation.
- Run critical tests on CI to check for regressions due to changes.
- coverage is viewed as a reference indicator and checks whether important operations are properly verified.