Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions keyword/Chapter10/React_Optimization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# React 최적화 정리: useMemo, useCallback, React.memo, Referential Equality

## Referential Equality (참조 동일성)

### 1. 정의

Referential Equality는 **두 값이 메모리에서 동일한 참조(주소)를 가리키는지** 판단하는 방식이다.

```js
const a = { value: 1 };
const b = { value: 1 };
console.log(a === b); // false (다른 참조)

const c = a;
console.log(a === c); // true (같은 참조)
```

- 객체, 배열, 함수는 참조형 데이터로 주소값을 비교 (`===`)
- 원시값은 값 자체를 비교

### 2. React 최적화와의 관계

- React는 `props`, `state` 등을 비교할 때 **참조 동일성** 기준으로 판단
- `React.memo`, `useCallback`, `useMemo`, `useEffect` 등의 **의존성 비교 기준도 참조 동일성**
- 같은 값을 새로 생성하면 참조가 달라져 **불필요한 리렌더링 발생**

```tsx
// 매번 새로운 객체 → memo 무효화
<Component data={{ id: 1 }} />
```

---

## useCallback

### 1. 개념 및 정의

`useCallback`은 함수형 컴포넌트가 리렌더링될 때 동일한 함수를 재사용할 수 있도록 하는 Hook이다.
즉, **함수의 참조값을 유지**해 자식 컴포넌트의 **불필요한 리렌더링 방지**에 사용된다.

```tsx
const memoizedCallback = useCallback(() => {
// 실행할 함수
}, [dependency1, dependency2]);
```

### 2. 사용 목적

- 자식에게 함수를 props로 전달할 때, 매번 새 함수가 생성되면 자식도 리렌더링됨
- `useCallback`을 사용하면 동일한 참조값을 유지할 수 있음

### 3. 의존성 배열

- `[]`: 컴포넌트가 처음 렌더링될 때만 생성
- `[state]`: state가 변경될 때마다 새 함수 생성

```tsx
// 잘못된 예시
const increment = useCallback(() => {
setCount(count + 1); // stale closure 가능성
}, []);

// 올바른 예시
const increment = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
```

### 4. 사용 예시

```tsx
const handleClick = useCallback(() => {
console.log("Clicked!");
}, []);

<ChildComponent onClick={handleClick} />;
```

### 5. 주의사항

- 함수가 캐싱되므로 메모리 사용
- 불필요한 사용은 오히려 성능 저하
- 의존성 배열 정확하게 작성해야 함

---

## React.memo

### 1. 개념 및 정의

- `React.memo`는 고차 컴포넌트로, **props가 바뀌지 않으면 리렌더링을 건너뛰는 기능**을 제공한다.

```tsx
const MemoizedComponent = React.memo(MyComponent);
```

### 2. 사용 목적

- 리렌더링 비용이 큰 컴포넌트에서 최적화를 위해 사용
- `props`가 **얕은 비교(shallow compare)**로 동일한 경우 렌더링 생략

### 3. 사용 예시

```tsx
const Button = React.memo(({ onClick, children }) => {
console.log("Button rendered");
return <button onClick={onClick}>{children}</button>;
});

const handleClick = useCallback(() => {
console.log("clicked");
}, []);

<Button onClick={handleClick}>Click</Button>;
```

### 4. 주의사항

- 객체, 배열, 함수는 참조값이 달라질 수 있어 memo가 무효화됨 → `useMemo`, `useCallback`과 함께 사용 필요
- 깊은 비교는 하지 않음 → 커스텀 비교 함수 필요 시 `React.memo(Component, areEqual)`

---

## useMemo

### 1. 개요

`useMemo`는 렌더링 시 **비용이 많이 드는 계산 결과를 기억(memoization)**하여
**불필요한 재계산을 방지**하고 컴포넌트 성능을 최적화하는 Hook이다.

### 2. 문법

```tsx
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
```

### 3. 주요 특징

- 리렌더링 시 의존성이 변하지 않으면 계산 생략
- 값(Value)을 메모이제이션
- `useCallback`과 달리 **함수 실행 결과(값)**을 캐싱

### 4. 사용 예시

```tsx
const primeList = useMemo(() => findPrimes(limit), [limit]);
const sortedList = useMemo(() => data.sort(customCompare), [data]);
```

### 5. 사용 시기

- 권장 시점: 정렬, 필터링, 계산 등 연산 비용이 클 때
- 비권장 시점: 간단한 연산, 렌더링이 드물 때
- 주의: 항상 새 값을 반환해야 하는 경우가 아니라면 불필요할 수 있음

### 6. 자주 발생하는 실수

- 빈 의존성 배열 사용 → 업데이트 반영 안 됨
- 단순 연산까지 useMemo로 감싸 성능 저하 및 복잡도 증가
25 changes: 25 additions & 0 deletions mission/Chapter10/SearchMovie/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.env
54 changes: 54 additions & 0 deletions mission/Chapter10/SearchMovie/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:

```js
export default tseslint.config({
extends: [
// Remove ...tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
],
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```

You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:

```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'

export default tseslint.config({
plugins: {
// Add the react-x and react-dom plugins
'react-x': reactX,
'react-dom': reactDom,
},
rules: {
// other rules...
// Enable its recommended typescript rules
...reactX.configs['recommended-typescript'].rules,
...reactDom.configs.recommended.rules,
},
})
```
13 changes: 13 additions & 0 deletions mission/Chapter10/SearchMovie/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
36 changes: 36 additions & 0 deletions mission/Chapter10/SearchMovie/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "shoppingcart",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@reduxjs/toolkit": "^2.8.2",
"@tailwindcss/vite": "^4.1.8",
"axios": "^1.10.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-redux": "^9.2.0",
"tailwindcss": "^4.1.8",
"zustand": "^5.0.5"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5"
}
}
1 change: 1 addition & 0 deletions mission/Chapter10/SearchMovie/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
11 changes: 11 additions & 0 deletions mission/Chapter10/SearchMovie/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Homepage from "./pages/Homepage";

function App() {
return (
<>
<Homepage />
</>
);
}

export default App;
8 changes: 8 additions & 0 deletions mission/Chapter10/SearchMovie/src/apis/axiosClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import axios from "axios";

export const axiosClient = axios.create({
baseURL : "https://api.themoviedb.org/3",
headers : {
Authorization : `Bearer ${import.meta.env.VITE_TMDB_TOKEN}`,
},
})
1 change: 1 addition & 0 deletions mission/Chapter10/SearchMovie/src/assets/react.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions mission/Chapter10/SearchMovie/src/components/Input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
interface InputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
}

export const Input = ({
value,
onChange,
placeholder = "검색어를 입력하세요.",
className,
}: InputProps) => {
return (
<input
className={`w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-blue-500 focus:ring-blue-500 ${className}`}
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
);
};
32 changes: 32 additions & 0 deletions mission/Chapter10/SearchMovie/src/components/LanguageSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
interface LanguageOption {
value: string;
label: string;
}

interface LanguageSelectProps {
value: string;
onChange: (value: string) => void;
options: LanguageOption[];
className?: string;
}

export const LanguageSelector = ({
value,
onChange,
options,
className = "",
}: LanguageSelectProps) => {
return (
<select
value={value}
onChange={(e) => onChange(e.target.value)}
className={`w-full rounded-lg border border-gray-300 px-4 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${className}`}
>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
};
Loading