From 99aadb1d4619ea4b13381f6113e94f7b950e5353 Mon Sep 17 00:00:00 2001 From: chyerin802 Date: Sun, 6 Oct 2024 14:19:19 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20=EC=96=95=EC=9D=80=20=EB=B9=84?= =?UTF-8?q?=EA=B5=90=20=ED=95=A8=EC=88=98=20`shallowEquals`=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/@lib/equalities/shallowEquals.ts | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/assignment/src/@lib/equalities/shallowEquals.ts b/packages/assignment/src/@lib/equalities/shallowEquals.ts index d772d470..9647c1da 100644 --- a/packages/assignment/src/@lib/equalities/shallowEquals.ts +++ b/packages/assignment/src/@lib/equalities/shallowEquals.ts @@ -1,4 +1,36 @@ +/** + * 두 값을 얕게 비교하는 함수 + * @param objA 첫 번째 비교 대상 값 + * @param objB 두 번째 비교 대상 값 + * @returns 두 값이 얕은 비교에서 동일하면 true, 그렇지 않으면 false + */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function shallowEquals(objA: any, objB: any): boolean { - return objA === objB; + // 1. 두 값이 같은 참조를 가지거나 값이 같으면 true 반환 + if (objA === objB) return true; + + // 2. 하나라도 객체가 아니거나 null인 경우 false 반환 + // (null의 typeof는 'object'지만 원시값이므로 따로 처리) + if ( + typeof objA !== "object" || + typeof objB !== "object" || + objA === null || + objB === null + ) { + return false; + } + + // 3. 객체의 키 배열을 구하고, 키 개수가 다르면 false 반환 + const keysA = Object.keys(objA); + const keysB = Object.keys(objB); + if (keysA.length !== keysB.length) return false; + + // 4. 모든 키에 대해 값을 비교 (첫번째 depth만) + for (const key of keysA) { + // - 다른 경우가 하나라도 있으면 false 반환 + if (objA[key] !== objB[key]) return false; + } + + // - 모든 키에 대해 값이 같으면 true 반환 + return true; } From fec135943d1e9b176c76fead7d0500b24ca10392 Mon Sep 17 00:00:00 2001 From: chyerin802 Date: Sun, 6 Oct 2024 14:20:27 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20=EA=B9=8A=EC=9D=80=20=EB=B9=84?= =?UTF-8?q?=EA=B5=90=20=ED=95=A8=EC=88=98=20`deepEquals`=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/@lib/equalities/deepEquals.ts | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/assignment/src/@lib/equalities/deepEquals.ts b/packages/assignment/src/@lib/equalities/deepEquals.ts index c50ae8b8..8291745d 100644 --- a/packages/assignment/src/@lib/equalities/deepEquals.ts +++ b/packages/assignment/src/@lib/equalities/deepEquals.ts @@ -1,4 +1,36 @@ +/** + * 두 값을 깊게 비교하는 함수 + * @param objA 첫 번째 비교 대상 값 + * @param objB 두 번째 비교 대상 값 + * @returns 두 값이 깊은 비교에서 동일하면 true, 그렇지 않으면 false + */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function deepEquals(objA: any, objB: any): boolean { - return objA === objB; + // 1. 두 값이 같은 참조를 가지거나 값이 같으면 true 반환 + if (objA === objB) return true; + + // 2. 하나라도 객체가 아니거나 null인 경우 false 반환 + // (null의 typeof는 'object'지만 원시값이므로 따로 처리) + if ( + typeof objA !== "object" || + typeof objB !== "object" || + objA === null || + objB === null + ) { + return false; + } + + // 3. 객체의 키 배열을 구하고, 키 개수가 다르면 false 반환 + const keysA = Object.keys(objA); + const keysB = Object.keys(objB); + if (keysA.length !== keysB.length) return false; + + // 4. 모든 키에 대해 값에 대해 재귀적으로 깊은 비교 + for (const key of keysA) { + // - 다른 경우가 하나라도 있으면 false 반환 + if (!deepEquals(objA[key], objB[key])) return false; + } + + // - 모든 키에 대해 값이 같으면 true 반환 + return true; } From 2be5f3ff28351910fd0951c503aa5ff2c831bffa Mon Sep 17 00:00:00 2001 From: chyerin Date: Mon, 7 Oct 2024 22:17:55 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20useRef=20=ED=9B=85=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/assignment/src/@lib/hooks/useRef.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/assignment/src/@lib/hooks/useRef.ts b/packages/assignment/src/@lib/hooks/useRef.ts index 2dc9e83f..46b93a59 100644 --- a/packages/assignment/src/@lib/hooks/useRef.ts +++ b/packages/assignment/src/@lib/hooks/useRef.ts @@ -1,4 +1,7 @@ +import { useState } from "react"; + export function useRef(initialValue: T): { current: T } { - // React의 useState를 이용해서 만들어보세요. - return { current: initialValue }; + // React의 useState를 이용하여 useRef 를 구현 + const [ref] = useState({ current: initialValue }); + return ref; } From 3af0a05aa877e1d0e217c7df7047d861e538d6c6 Mon Sep 17 00:00:00 2001 From: chyerin Date: Mon, 7 Oct 2024 23:46:41 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20useMemo=20=ED=9B=85=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/assignment/src/@lib/hooks/useMemo.ts | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/assignment/src/@lib/hooks/useMemo.ts b/packages/assignment/src/@lib/hooks/useMemo.ts index 75febc3b..48966833 100644 --- a/packages/assignment/src/@lib/hooks/useMemo.ts +++ b/packages/assignment/src/@lib/hooks/useMemo.ts @@ -1,10 +1,27 @@ import { DependencyList } from "react"; +import { useRef } from "./useRef"; import { shallowEquals } from "../equalities"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export function useMemo(factory: () => T, deps: DependencyList, equals = shallowEquals): T { - // 직접 작성한 useRef를 통해서 만들어보세요. - return factory(); +// useMemo 훅은 계산 비용이 높은 값을 메모이제이션합니다. +export function useMemo( + factory: () => T, + deps: DependencyList, + equals = shallowEquals +): T { + // 1. 이전 의존성과 결과를 저장할 ref 생성 + const memoizedRef = useRef<{ + result?: T; + deps?: DependencyList; + }>({}); + + // 2. 새로운 의존성과 결과를 저장한다 + // - 아직 초기화 되지 않은 경우 + // - 의존성이 변경된 경우 + if (!memoizedRef.current.deps || !equals(memoizedRef.current.deps, deps)) { + memoizedRef.current.result = factory(); + memoizedRef.current.deps = deps; + } + + // 4. 메모이제이션된 값 반환 + return memoizedRef.current.result as T; } From a88dfe3ee06cb5870b48ffc9ed1d26e4fef8760f Mon Sep 17 00:00:00 2001 From: chyerin Date: Wed, 9 Oct 2024 04:28:41 +0900 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20useCallback=20=ED=9B=85=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/assignment/src/@lib/hooks/useCallback.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/assignment/src/@lib/hooks/useCallback.ts b/packages/assignment/src/@lib/hooks/useCallback.ts index 3ff768c9..5d2ca515 100644 --- a/packages/assignment/src/@lib/hooks/useCallback.ts +++ b/packages/assignment/src/@lib/hooks/useCallback.ts @@ -1,9 +1,11 @@ import { DependencyList } from "react"; +import { useMemo } from "./useMemo"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-explicit-any -export function useCallback any>(factory: T, deps: DependencyList): T { - // 직접 작성한 useMemo를 통해서 만들어보세요. - return ((...args) => factory(...args)) as T +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function useCallback any>( + factory: T, + deps: DependencyList +): T { + // eslint-disable-next-line react-hooks/exhaustive-deps + return useMemo(() => factory, deps); } From 64f2afb7e67c31a275e3283acd3a25aa56652797 Mon Sep 17 00:00:00 2001 From: chyerin Date: Wed, 9 Oct 2024 04:30:20 +0900 Subject: [PATCH 06/11] =?UTF-8?q?feat:=20useDeepMemo=20=ED=9B=85=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/assignment/src/@lib/hooks/useDeepMemo.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/assignment/src/@lib/hooks/useDeepMemo.ts b/packages/assignment/src/@lib/hooks/useDeepMemo.ts index 2713bf10..02aae031 100644 --- a/packages/assignment/src/@lib/hooks/useDeepMemo.ts +++ b/packages/assignment/src/@lib/hooks/useDeepMemo.ts @@ -2,10 +2,7 @@ import { DependencyList } from "react"; import { useMemo } from "./useMemo"; import { deepEquals } from "../equalities"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore - export function useDeepMemo(factory: () => T, deps: DependencyList): T { - // 직접 작성한 useMemo를 참고해서 만들어보세요. - return useMemo(factory, deps, deepEquals) + // eslint-disable-next-line react-hooks/exhaustive-deps + return useMemo(factory, deps, deepEquals); } From d6c1032b9139660a6559ab694c6af17e45eab905 Mon Sep 17 00:00:00 2001 From: chyerin Date: Wed, 9 Oct 2024 16:48:24 +0900 Subject: [PATCH 07/11] =?UTF-8?q?feat:=20memo=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/assignment/src/@lib/hocs/memo.ts | 12 ---------- packages/assignment/src/@lib/hocs/memo.tsx | 27 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 12 deletions(-) delete mode 100644 packages/assignment/src/@lib/hocs/memo.ts create mode 100644 packages/assignment/src/@lib/hocs/memo.tsx diff --git a/packages/assignment/src/@lib/hocs/memo.ts b/packages/assignment/src/@lib/hocs/memo.ts deleted file mode 100644 index 0a7ebaae..00000000 --- a/packages/assignment/src/@lib/hocs/memo.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { shallowEquals } from "../equalities"; -import { ComponentType } from "react"; - -export function memo

( - Component: ComponentType

, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unused-vars - equals = shallowEquals -) { - return Component; -} diff --git a/packages/assignment/src/@lib/hocs/memo.tsx b/packages/assignment/src/@lib/hocs/memo.tsx new file mode 100644 index 00000000..8b7e5fc7 --- /dev/null +++ b/packages/assignment/src/@lib/hocs/memo.tsx @@ -0,0 +1,27 @@ +import { ReactNode, ComponentType } from "react"; +import { shallowEquals } from "../equalities"; +import { useRef } from "../hooks"; + +export function memo

( + Component: ComponentType

, + equals = shallowEquals +) { + return (props: P) => { + // 1. 이전 props와 렌더링 결과를 저장할 ref 생성 + const memoizedRef = useRef<{ props?: P; element?: ReactNode }>({}); + + // 2. 새로운 props와 렌더링 결과를 저장한다 + // - 아직 초기화 되지 않은 경우 + // - props가 변경된 경우 + if ( + !memoizedRef.current.props || + !equals(props, memoizedRef.current.props) + ) { + memoizedRef.current.props = props; + memoizedRef.current.element = ; + } + + // 3. 렌더링 결과를 반환한다 + return memoizedRef.current.element; + }; +} From 790d410c0c6e967b3d665f23003d4141208ba647 Mon Sep 17 00:00:00 2001 From: chyerin Date: Wed, 9 Oct 2024 16:48:39 +0900 Subject: [PATCH 08/11] =?UTF-8?q?feat:=20deepMemo=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/assignment/src/@lib/hocs/deepMemo.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/assignment/src/@lib/hocs/deepMemo.ts b/packages/assignment/src/@lib/hocs/deepMemo.ts index 4a1f29c4..6960f0ac 100644 --- a/packages/assignment/src/@lib/hocs/deepMemo.ts +++ b/packages/assignment/src/@lib/hocs/deepMemo.ts @@ -1,9 +1,7 @@ import { deepEquals } from "../equalities"; import { ComponentType } from "react"; -import { memo } from "./memo.ts"; +import { memo } from "./memo"; -export function deepMemo

( - Component: ComponentType

, -) { +export function deepMemo

(Component: ComponentType

) { return memo(Component, deepEquals); } From c2c929fcc1cd693ffbcd2d080da0cd4b06c57467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B1=84=EC=98=88=EB=A6=B0?= Date: Wed, 9 Oct 2024 21:12:29 +0900 Subject: [PATCH 09/11] =?UTF-8?q?refactor:=20App=20=EC=97=90=EC=84=9C=20co?= =?UTF-8?q?ntext,=20type,=20component=20=EB=B6=84=EB=A6=AC=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: user, notification 타입 분리 * feat: context 관련 파일 분리 및 AppContext 구현 - useAppContext 훅, AppProvider 구현 * feat: 하위 컴포넌트 분리 * refactor: App 컴포넌트에서 컨텍스트, 컴포넌트, 타입 관련 코드 분리 --------- Co-authored-by: chyerin --- packages/assignment/src/App.tsx | 305 ++---------------- .../assignment/src/components/ComplexForm.tsx | 87 +++++ packages/assignment/src/components/Header.tsx | 47 +++ .../assignment/src/components/ItemList.tsx | 52 +++ .../src/components/NotificationSystem.tsx | 34 ++ packages/assignment/src/components/index.ts | 4 + .../assignment/src/contexts/app/AppContext.ts | 17 + .../src/contexts/app/AppProvider.tsx | 59 ++++ packages/assignment/src/contexts/app/index.ts | 3 + .../src/contexts/app/useAppContext.ts | 13 + packages/assignment/src/contexts/index.ts | 1 + packages/assignment/src/types/index.ts | 2 + packages/assignment/src/types/notification.ts | 5 + packages/assignment/src/types/user.ts | 5 + 14 files changed, 358 insertions(+), 276 deletions(-) create mode 100644 packages/assignment/src/components/ComplexForm.tsx create mode 100644 packages/assignment/src/components/Header.tsx create mode 100644 packages/assignment/src/components/ItemList.tsx create mode 100644 packages/assignment/src/components/NotificationSystem.tsx create mode 100644 packages/assignment/src/components/index.ts create mode 100644 packages/assignment/src/contexts/app/AppContext.ts create mode 100644 packages/assignment/src/contexts/app/AppProvider.tsx create mode 100644 packages/assignment/src/contexts/app/index.ts create mode 100644 packages/assignment/src/contexts/app/useAppContext.ts create mode 100644 packages/assignment/src/contexts/index.ts create mode 100644 packages/assignment/src/types/index.ts create mode 100644 packages/assignment/src/types/notification.ts create mode 100644 packages/assignment/src/types/user.ts diff --git a/packages/assignment/src/App.tsx b/packages/assignment/src/App.tsx index 1c36c626..1090277c 100644 --- a/packages/assignment/src/App.tsx +++ b/packages/assignment/src/App.tsx @@ -1,288 +1,41 @@ -import React, { useState, createContext, useContext } from 'react'; -import { generateItems, renderLog } from './utils'; +import React, { useState } from 'react' +import { generateItems } from './utils' +import { AppProvider, useAppContext } from './contexts' -// 타입 정의 -interface Item { - id: number; - name: string; - category: string; - price: number; -} - -interface User { - id: number; - name: string; - email: string; -} +// components +import { ItemList, Header, ComplexForm, NotificationSystem } from './components' -interface Notification { - id: number; - message: string; - type: 'info' | 'success' | 'warning' | 'error'; -} - -// AppContext 타입 정의 -interface AppContextType { - theme: string; - toggleTheme: () => void; - user: User | null; - login: (email: string, password: string) => void; - logout: () => void; - notifications: Notification[]; - addNotification: (message: string, type: Notification['type']) => void; - removeNotification: (id: number) => void; -} - -const AppContext = createContext(undefined); - -// 커스텀 훅: useAppContext -const useAppContext = () => { - const context = useContext(AppContext); - if (context === undefined) { - throw new Error('useAppContext must be used within an AppProvider'); - } - return context; -}; - -// Header 컴포넌트 -export const Header: React.FC = () => { - renderLog('Header rendered'); - const { theme, toggleTheme, user, login, logout } = useAppContext(); - - const handleLogin = () => { - // 실제 애플리케이션에서는 사용자 입력을 받아야 합니다. - login('user@example.com', 'password'); - }; +const AppContent: React.FC = () => { + const { theme } = useAppContext() + const [items] = useState(generateItems(10000)) return ( -

-
-

샘플 애플리케이션

-
- - {user ? ( -
- {user.name}님 환영합니다! - -
- ) : ( - - )} +
+
+
+
+
+ +
+
+ +
-
- ); -}; - -// ItemList 컴포넌트 -export const ItemList: React.FC<{ items: Item[] }> = ({ items }) => { - renderLog('ItemList rendered'); - const [filter, setFilter] = useState(''); - const { theme } = useAppContext(); - - const filteredItems = items.filter(item => - item.name.toLowerCase().includes(filter.toLowerCase()) || - item.category.toLowerCase().includes(filter.toLowerCase()) - ); - - const averagePrice = items.reduce((sum, item) => sum + item.price, 0) / items.length; - - return ( -
-

상품 목록

- setFilter(e.target.value)} - className="w-full p-2 mb-4 border border-gray-300 rounded text-black" - /> -

평균 가격: {averagePrice.toLocaleString()}원

-
    - {filteredItems.slice(0, 100).map(item => ( -
  • - {item.name} - {item.category} - {item.price.toLocaleString()}원 -
  • - ))} -
- {filteredItems.length > 100 &&

...그 외 {filteredItems.length - 100}개 상품

} -
- ); -}; - -// ComplexForm 컴포넌트 -export const ComplexForm: React.FC = () => { - renderLog('ComplexForm rendered'); - const { addNotification } = useAppContext(); - const [formData, setFormData] = useState({ - name: '', - email: '', - age: 0, - preferences: [] as string[] - }); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - addNotification('폼이 성공적으로 제출되었습니다', 'success'); - }; - - const handleInputChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setFormData(prev => ({ - ...prev, - [name]: name === 'age' ? parseInt(value) || 0 : value - })); - }; - - const handlePreferenceChange = (preference: string) => { - setFormData(prev => ({ - ...prev, - preferences: prev.preferences.includes(preference) - ? prev.preferences.filter(p => p !== preference) - : [...prev.preferences, preference] - })); - }; - - return ( -
-

복잡한 폼

-
- - - -
- {['독서', '운동', '음악', '여행'].map(pref => ( - - ))} -
- -
+
- ); -}; - -// NotificationSystem 컴포넌트 -export const NotificationSystem: React.FC = () => { - renderLog('NotificationSystem rendered'); - const { notifications, removeNotification } = useAppContext(); - - return ( -
- {notifications.map(notification => ( -
- {notification.message} - -
- ))} -
- ); -}; + ) +} // 메인 App 컴포넌트 const App: React.FC = () => { - const [theme, setTheme] = useState('light'); - const [items] = useState(generateItems(10000)); - const [user, setUser] = useState(null); - const [notifications, setNotifications] = useState([]); - - const toggleTheme = () => { - setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light'); - }; - - const login = (email: string) => { - setUser({ id: 1, name: '홍길동', email }); - addNotification('성공적으로 로그인되었습니다', 'success'); - }; - - const logout = () => { - setUser(null); - addNotification('로그아웃되었습니다', 'info'); - }; - - const addNotification = (message: string, type: Notification['type']) => { - const newNotification: Notification = { - id: Date.now(), - message, - type - }; - setNotifications(prev => [...prev, newNotification]); - }; - - const removeNotification = (id: number) => { - setNotifications(prev => prev.filter(notification => notification.id !== id)); - }; - - const contextValue: AppContextType = { - theme, - toggleTheme, - user, - login, - logout, - notifications, - addNotification, - removeNotification - }; - return ( - -
-
-
-
-
- -
-
- -
-
-
- -
-
- ); -}; + + + + ) +} -export default App; +export default App diff --git a/packages/assignment/src/components/ComplexForm.tsx b/packages/assignment/src/components/ComplexForm.tsx new file mode 100644 index 00000000..ba4343b2 --- /dev/null +++ b/packages/assignment/src/components/ComplexForm.tsx @@ -0,0 +1,87 @@ +import { useState } from 'react' +import { renderLog } from '../utils' +import { useAppContext } from '../contexts' + +export const ComplexForm: React.FC = () => { + renderLog('ComplexForm rendered') + const { addNotification } = useAppContext() + const [formData, setFormData] = useState({ + name: '', + email: '', + age: 0, + preferences: [] as string[], + }) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + addNotification('폼이 성공적으로 제출되었습니다', 'success') + } + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setFormData((prev) => ({ + ...prev, + [name]: name === 'age' ? parseInt(value) || 0 : value, + })) + } + + const handlePreferenceChange = (preference: string) => { + setFormData((prev) => ({ + ...prev, + preferences: prev.preferences.includes(preference) + ? prev.preferences.filter((p) => p !== preference) + : [...prev.preferences, preference], + })) + } + + return ( +
+

복잡한 폼

+
+ + + +
+ {['독서', '운동', '음악', '여행'].map((pref) => ( + + ))} +
+ +
+
+ ) +} diff --git a/packages/assignment/src/components/Header.tsx b/packages/assignment/src/components/Header.tsx new file mode 100644 index 00000000..a723fd52 --- /dev/null +++ b/packages/assignment/src/components/Header.tsx @@ -0,0 +1,47 @@ +import { renderLog } from '../utils' +import { useAppContext } from '../contexts' + +// Header 컴포넌트 +export const Header: React.FC = () => { + renderLog('Header rendered') + const { theme, toggleTheme, user, login, logout } = useAppContext() + + const handleLogin = () => { + // 실제 애플리케이션에서는 사용자 입력을 받아야 합니다. + login('user@example.com', 'password') + } + + return ( +
+
+

샘플 애플리케이션

+
+ + {user ? ( +
+ {user.name}님 환영합니다! + +
+ ) : ( + + )} +
+
+
+ ) +} diff --git a/packages/assignment/src/components/ItemList.tsx b/packages/assignment/src/components/ItemList.tsx new file mode 100644 index 00000000..9f80e2fe --- /dev/null +++ b/packages/assignment/src/components/ItemList.tsx @@ -0,0 +1,52 @@ +import { useState } from 'react' +import { renderLog } from '../utils' +import { useAppContext } from '../contexts' + +interface Item { + id: number + name: string + category: string + price: number +} + +export const ItemList: React.FC<{ items: Item[] }> = ({ items }) => { + renderLog('ItemList rendered') + const [filter, setFilter] = useState('') + const { theme } = useAppContext() + + const filteredItems = items.filter( + (item) => + item.name.toLowerCase().includes(filter.toLowerCase()) || + item.category.toLowerCase().includes(filter.toLowerCase()) + ) + + const averagePrice = + items.reduce((sum, item) => sum + item.price, 0) / items.length + + return ( +
+

상품 목록

+ setFilter(e.target.value)} + className="w-full p-2 mb-4 border border-gray-300 rounded text-black" + /> +

평균 가격: {averagePrice.toLocaleString()}원

+
    + {filteredItems.slice(0, 100).map((item) => ( +
  • + {item.name} - {item.category} - {item.price.toLocaleString()}원 +
  • + ))} +
+ {filteredItems.length > 100 && ( +

...그 외 {filteredItems.length - 100}개 상품

+ )} +
+ ) +} diff --git a/packages/assignment/src/components/NotificationSystem.tsx b/packages/assignment/src/components/NotificationSystem.tsx new file mode 100644 index 00000000..abb11cbb --- /dev/null +++ b/packages/assignment/src/components/NotificationSystem.tsx @@ -0,0 +1,34 @@ +import { renderLog } from '../utils' +import { useAppContext } from '../contexts' + +export const NotificationSystem: React.FC = () => { + renderLog('NotificationSystem rendered') + const { notifications, removeNotification } = useAppContext() + + return ( +
+ {notifications.map((notification) => ( +
+ {notification.message} + +
+ ))} +
+ ) +} diff --git a/packages/assignment/src/components/index.ts b/packages/assignment/src/components/index.ts new file mode 100644 index 00000000..ea02936a --- /dev/null +++ b/packages/assignment/src/components/index.ts @@ -0,0 +1,4 @@ +export * from './ComplexForm' +export * from './Header' +export * from './ItemList' +export * from './NotificationSystem' diff --git a/packages/assignment/src/contexts/app/AppContext.ts b/packages/assignment/src/contexts/app/AppContext.ts new file mode 100644 index 00000000..1924245e --- /dev/null +++ b/packages/assignment/src/contexts/app/AppContext.ts @@ -0,0 +1,17 @@ +import { createContext } from 'react' +import { Notification, User } from '../../types' + +// AppContext 타입 정의 +export interface AppContextType { + theme: string + toggleTheme: () => void + user: User | null + login: (email: string, password: string) => void + logout: () => void + notifications: Notification[] + addNotification: (message: string, type: Notification['type']) => void + removeNotification: (id: number) => void +} + +const AppContext = createContext(undefined) +export default AppContext diff --git a/packages/assignment/src/contexts/app/AppProvider.tsx b/packages/assignment/src/contexts/app/AppProvider.tsx new file mode 100644 index 00000000..58c240f4 --- /dev/null +++ b/packages/assignment/src/contexts/app/AppProvider.tsx @@ -0,0 +1,59 @@ +import React, { useState, ReactNode } from 'react' +import AppContext, { AppContextType } from './AppContext' +import { User, Notification } from '../../types' + +interface AppProviderProps { + children: ReactNode +} + +const AppProvider: React.FC = ({ children }) => { + const [theme, setTheme] = useState('light') + const [user, setUser] = useState(null) + const [notifications, setNotifications] = useState([]) + + const toggleTheme = () => { + setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light')) + } + + const login = (email: string) => { + setUser({ id: 1, name: '홍길동', email }) + addNotification('성공적으로 로그인되었습니다', 'success') + } + + const logout = () => { + setUser(null) + addNotification('로그아웃되었습니다', 'info') + } + + const addNotification = (message: string, type: Notification['type']) => { + const newNotification: Notification = { + id: Date.now(), + message, + type, + } + setNotifications((prev) => [...prev, newNotification]) + } + + const removeNotification = (id: number) => { + setNotifications((prev) => + prev.filter((notification) => notification.id !== id) + ) + } + + const contextValue: AppContextType = { + theme, + toggleTheme, + user, + login, + logout, + notifications, + addNotification, + removeNotification, + } + + return ( + {children} + ) +} + +export default AppProvider diff --git a/packages/assignment/src/contexts/app/index.ts b/packages/assignment/src/contexts/app/index.ts new file mode 100644 index 00000000..6b075c44 --- /dev/null +++ b/packages/assignment/src/contexts/app/index.ts @@ -0,0 +1,3 @@ +export { default as AppContext } from './AppContext' +export { default as useAppContext } from './useAppContext' +export { default as AppProvider } from './AppProvider' diff --git a/packages/assignment/src/contexts/app/useAppContext.ts b/packages/assignment/src/contexts/app/useAppContext.ts new file mode 100644 index 00000000..5b118129 --- /dev/null +++ b/packages/assignment/src/contexts/app/useAppContext.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react' +import AppContext from './AppContext' + +// 커스텀 훅: useAppContext +const useAppContext = () => { + const context = useContext(AppContext) + if (context === undefined) { + throw new Error('useAppContext must be used within an AppProvider') + } + return context +} + +export default useAppContext diff --git a/packages/assignment/src/contexts/index.ts b/packages/assignment/src/contexts/index.ts new file mode 100644 index 00000000..57a83383 --- /dev/null +++ b/packages/assignment/src/contexts/index.ts @@ -0,0 +1 @@ +export * from './app' diff --git a/packages/assignment/src/types/index.ts b/packages/assignment/src/types/index.ts new file mode 100644 index 00000000..18eafb67 --- /dev/null +++ b/packages/assignment/src/types/index.ts @@ -0,0 +1,2 @@ +export * from './notification' +export * from './user' diff --git a/packages/assignment/src/types/notification.ts b/packages/assignment/src/types/notification.ts new file mode 100644 index 00000000..715f0d14 --- /dev/null +++ b/packages/assignment/src/types/notification.ts @@ -0,0 +1,5 @@ +export interface Notification { + id: number + message: string + type: 'info' | 'success' | 'warning' | 'error' +} diff --git a/packages/assignment/src/types/user.ts b/packages/assignment/src/types/user.ts new file mode 100644 index 00000000..7372ad3f --- /dev/null +++ b/packages/assignment/src/types/user.ts @@ -0,0 +1,5 @@ +export interface User { + id: number + name: string + email: string +} From 96acf69fc1a62f343835073cd8b3b632c8618194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B1=84=EC=98=88=EB=A6=B0?= Date: Thu, 10 Oct 2024 00:56:38 +0900 Subject: [PATCH 10/11] =?UTF-8?q?feat:=20Theme,=20User,=20Notification=20c?= =?UTF-8?q?ontext=20=EA=B5=AC=ED=98=84=20(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: theme context 구현 * feat: notification context 구현 * feat: user context 구현 * style: 주석 제거 * refactor: context 관련 파일 named export로 변경 * refactor: App context 제거 및 다른 context 로 대체 --------- Co-authored-by: chyerin --- packages/assignment/src/App.tsx | 19 ++++-- .../assignment/src/components/ComplexForm.tsx | 4 +- packages/assignment/src/components/Header.tsx | 5 +- .../assignment/src/components/ItemList.tsx | 4 +- .../src/components/NotificationSystem.tsx | 4 +- .../assignment/src/contexts/app/AppContext.ts | 17 ------ .../src/contexts/app/AppProvider.tsx | 59 ------------------- packages/assignment/src/contexts/app/index.ts | 3 - .../src/contexts/app/useAppContext.ts | 13 ---- packages/assignment/src/contexts/index.ts | 4 +- .../notification/NotificationContext.ts | 12 ++++ .../notification/NotificationProvider.tsx | 43 ++++++++++++++ .../src/contexts/notification/index.ts | 3 + .../notification/useNotificationContext.ts | 12 ++++ .../src/contexts/theme/ThemeContext.ts | 10 ++++ .../src/contexts/theme/ThemeProvider.tsx | 25 ++++++++ .../assignment/src/contexts/theme/index.ts | 3 + .../src/contexts/theme/useThemeContext.ts | 10 ++++ .../src/contexts/user/UserContext.ts | 10 ++++ .../src/contexts/user/UserProvider.tsx | 33 +++++++++++ .../assignment/src/contexts/user/index.ts | 3 + .../src/contexts/user/useUserContext.ts | 10 ++++ 22 files changed, 200 insertions(+), 106 deletions(-) delete mode 100644 packages/assignment/src/contexts/app/AppContext.ts delete mode 100644 packages/assignment/src/contexts/app/AppProvider.tsx delete mode 100644 packages/assignment/src/contexts/app/index.ts delete mode 100644 packages/assignment/src/contexts/app/useAppContext.ts create mode 100644 packages/assignment/src/contexts/notification/NotificationContext.ts create mode 100644 packages/assignment/src/contexts/notification/NotificationProvider.tsx create mode 100644 packages/assignment/src/contexts/notification/index.ts create mode 100644 packages/assignment/src/contexts/notification/useNotificationContext.ts create mode 100644 packages/assignment/src/contexts/theme/ThemeContext.ts create mode 100644 packages/assignment/src/contexts/theme/ThemeProvider.tsx create mode 100644 packages/assignment/src/contexts/theme/index.ts create mode 100644 packages/assignment/src/contexts/theme/useThemeContext.ts create mode 100644 packages/assignment/src/contexts/user/UserContext.ts create mode 100644 packages/assignment/src/contexts/user/UserProvider.tsx create mode 100644 packages/assignment/src/contexts/user/index.ts create mode 100644 packages/assignment/src/contexts/user/useUserContext.ts diff --git a/packages/assignment/src/App.tsx b/packages/assignment/src/App.tsx index 1090277c..f5fc9572 100644 --- a/packages/assignment/src/App.tsx +++ b/packages/assignment/src/App.tsx @@ -1,12 +1,17 @@ import React, { useState } from 'react' import { generateItems } from './utils' -import { AppProvider, useAppContext } from './contexts' +import { + ThemeProvider, + UserProvider, + NotificationProvider, + useThemeContext, +} from './contexts' // components import { ItemList, Header, ComplexForm, NotificationSystem } from './components' const AppContent: React.FC = () => { - const { theme } = useAppContext() + const { theme } = useThemeContext() const [items] = useState(generateItems(10000)) return ( @@ -32,9 +37,13 @@ const AppContent: React.FC = () => { // 메인 App 컴포넌트 const App: React.FC = () => { return ( - - - + + + + + + + ) } diff --git a/packages/assignment/src/components/ComplexForm.tsx b/packages/assignment/src/components/ComplexForm.tsx index ba4343b2..495b6d8a 100644 --- a/packages/assignment/src/components/ComplexForm.tsx +++ b/packages/assignment/src/components/ComplexForm.tsx @@ -1,10 +1,10 @@ import { useState } from 'react' import { renderLog } from '../utils' -import { useAppContext } from '../contexts' +import { useNotificationContext } from '../contexts' export const ComplexForm: React.FC = () => { renderLog('ComplexForm rendered') - const { addNotification } = useAppContext() + const { addNotification } = useNotificationContext() const [formData, setFormData] = useState({ name: '', email: '', diff --git a/packages/assignment/src/components/Header.tsx b/packages/assignment/src/components/Header.tsx index a723fd52..44d88269 100644 --- a/packages/assignment/src/components/Header.tsx +++ b/packages/assignment/src/components/Header.tsx @@ -1,10 +1,11 @@ import { renderLog } from '../utils' -import { useAppContext } from '../contexts' +import { useThemeContext, useUserContext } from '../contexts' // Header 컴포넌트 export const Header: React.FC = () => { renderLog('Header rendered') - const { theme, toggleTheme, user, login, logout } = useAppContext() + const {theme,toggleTheme} = useThemeContext() + const {user, login, logout} = useUserContext() const handleLogin = () => { // 실제 애플리케이션에서는 사용자 입력을 받아야 합니다. diff --git a/packages/assignment/src/components/ItemList.tsx b/packages/assignment/src/components/ItemList.tsx index 9f80e2fe..92e1a27b 100644 --- a/packages/assignment/src/components/ItemList.tsx +++ b/packages/assignment/src/components/ItemList.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { renderLog } from '../utils' -import { useAppContext } from '../contexts' +import { useThemeContext } from '../contexts' interface Item { id: number @@ -12,7 +12,7 @@ interface Item { export const ItemList: React.FC<{ items: Item[] }> = ({ items }) => { renderLog('ItemList rendered') const [filter, setFilter] = useState('') - const { theme } = useAppContext() + const { theme } = useThemeContext() const filteredItems = items.filter( (item) => diff --git a/packages/assignment/src/components/NotificationSystem.tsx b/packages/assignment/src/components/NotificationSystem.tsx index abb11cbb..351a553c 100644 --- a/packages/assignment/src/components/NotificationSystem.tsx +++ b/packages/assignment/src/components/NotificationSystem.tsx @@ -1,9 +1,9 @@ import { renderLog } from '../utils' -import { useAppContext } from '../contexts' +import { useNotificationContext } from '../contexts' export const NotificationSystem: React.FC = () => { renderLog('NotificationSystem rendered') - const { notifications, removeNotification } = useAppContext() + const { notifications, removeNotification } = useNotificationContext() return (
diff --git a/packages/assignment/src/contexts/app/AppContext.ts b/packages/assignment/src/contexts/app/AppContext.ts deleted file mode 100644 index 1924245e..00000000 --- a/packages/assignment/src/contexts/app/AppContext.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createContext } from 'react' -import { Notification, User } from '../../types' - -// AppContext 타입 정의 -export interface AppContextType { - theme: string - toggleTheme: () => void - user: User | null - login: (email: string, password: string) => void - logout: () => void - notifications: Notification[] - addNotification: (message: string, type: Notification['type']) => void - removeNotification: (id: number) => void -} - -const AppContext = createContext(undefined) -export default AppContext diff --git a/packages/assignment/src/contexts/app/AppProvider.tsx b/packages/assignment/src/contexts/app/AppProvider.tsx deleted file mode 100644 index 58c240f4..00000000 --- a/packages/assignment/src/contexts/app/AppProvider.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { useState, ReactNode } from 'react' -import AppContext, { AppContextType } from './AppContext' -import { User, Notification } from '../../types' - -interface AppProviderProps { - children: ReactNode -} - -const AppProvider: React.FC = ({ children }) => { - const [theme, setTheme] = useState('light') - const [user, setUser] = useState(null) - const [notifications, setNotifications] = useState([]) - - const toggleTheme = () => { - setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light')) - } - - const login = (email: string) => { - setUser({ id: 1, name: '홍길동', email }) - addNotification('성공적으로 로그인되었습니다', 'success') - } - - const logout = () => { - setUser(null) - addNotification('로그아웃되었습니다', 'info') - } - - const addNotification = (message: string, type: Notification['type']) => { - const newNotification: Notification = { - id: Date.now(), - message, - type, - } - setNotifications((prev) => [...prev, newNotification]) - } - - const removeNotification = (id: number) => { - setNotifications((prev) => - prev.filter((notification) => notification.id !== id) - ) - } - - const contextValue: AppContextType = { - theme, - toggleTheme, - user, - login, - logout, - notifications, - addNotification, - removeNotification, - } - - return ( - {children} - ) -} - -export default AppProvider diff --git a/packages/assignment/src/contexts/app/index.ts b/packages/assignment/src/contexts/app/index.ts deleted file mode 100644 index 6b075c44..00000000 --- a/packages/assignment/src/contexts/app/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as AppContext } from './AppContext' -export { default as useAppContext } from './useAppContext' -export { default as AppProvider } from './AppProvider' diff --git a/packages/assignment/src/contexts/app/useAppContext.ts b/packages/assignment/src/contexts/app/useAppContext.ts deleted file mode 100644 index 5b118129..00000000 --- a/packages/assignment/src/contexts/app/useAppContext.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useContext } from 'react' -import AppContext from './AppContext' - -// 커스텀 훅: useAppContext -const useAppContext = () => { - const context = useContext(AppContext) - if (context === undefined) { - throw new Error('useAppContext must be used within an AppProvider') - } - return context -} - -export default useAppContext diff --git a/packages/assignment/src/contexts/index.ts b/packages/assignment/src/contexts/index.ts index 57a83383..8cc24f68 100644 --- a/packages/assignment/src/contexts/index.ts +++ b/packages/assignment/src/contexts/index.ts @@ -1 +1,3 @@ -export * from './app' +export * from './theme' +export * from './notification' +export * from './user' diff --git a/packages/assignment/src/contexts/notification/NotificationContext.ts b/packages/assignment/src/contexts/notification/NotificationContext.ts new file mode 100644 index 00000000..845706c0 --- /dev/null +++ b/packages/assignment/src/contexts/notification/NotificationContext.ts @@ -0,0 +1,12 @@ +import { createContext } from 'react' +import { Notification } from '../../types' + +export interface NotificationContextType { + notifications: Notification[] + addNotification: (message: string, type: Notification['type']) => void + removeNotification: (id: number) => void +} + +export const NotificationContext = createContext< + NotificationContextType | undefined +>(undefined) diff --git a/packages/assignment/src/contexts/notification/NotificationProvider.tsx b/packages/assignment/src/contexts/notification/NotificationProvider.tsx new file mode 100644 index 00000000..c60d4e0a --- /dev/null +++ b/packages/assignment/src/contexts/notification/NotificationProvider.tsx @@ -0,0 +1,43 @@ +import React, { useState, ReactNode } from 'react' +import { + NotificationContext, + NotificationContextType, +} from './NotificationContext' +import { Notification } from '../../types' + +interface NotificationProviderProps { + children: ReactNode +} + +export const NotificationProvider: React.FC = ({ + children, +}) => { + const [notifications, setNotifications] = useState([]) + + const addNotification = (message: string, type: Notification['type']) => { + const newNotification: Notification = { + id: Date.now(), + message, + type, + } + setNotifications((prev) => [...prev, newNotification]) + } + + const removeNotification = (id: number) => { + setNotifications((prev) => + prev.filter((notification) => notification.id !== id) + ) + } + + const contextValue: NotificationContextType = { + notifications, + addNotification, + removeNotification, + } + + return ( + + {children} + + ) +} diff --git a/packages/assignment/src/contexts/notification/index.ts b/packages/assignment/src/contexts/notification/index.ts new file mode 100644 index 00000000..bec3a2b1 --- /dev/null +++ b/packages/assignment/src/contexts/notification/index.ts @@ -0,0 +1,3 @@ +export * from './NotificationContext' +export * from './useNotificationContext' +export * from './NotificationProvider' diff --git a/packages/assignment/src/contexts/notification/useNotificationContext.ts b/packages/assignment/src/contexts/notification/useNotificationContext.ts new file mode 100644 index 00000000..1980cb80 --- /dev/null +++ b/packages/assignment/src/contexts/notification/useNotificationContext.ts @@ -0,0 +1,12 @@ +import { useContext } from 'react' +import { NotificationContext } from './NotificationContext' + +export const useNotificationContext = () => { + const context = useContext(NotificationContext) + if (context === undefined) { + throw new Error( + 'useNotificationContext must be used within an NotificationProvider' + ) + } + return context +} diff --git a/packages/assignment/src/contexts/theme/ThemeContext.ts b/packages/assignment/src/contexts/theme/ThemeContext.ts new file mode 100644 index 00000000..757121dd --- /dev/null +++ b/packages/assignment/src/contexts/theme/ThemeContext.ts @@ -0,0 +1,10 @@ +import { createContext } from 'react' + +export interface ThemeContextType { + theme: string + toggleTheme: () => void +} + +export const ThemeContext = createContext( + undefined +) diff --git a/packages/assignment/src/contexts/theme/ThemeProvider.tsx b/packages/assignment/src/contexts/theme/ThemeProvider.tsx new file mode 100644 index 00000000..b8b9a9c7 --- /dev/null +++ b/packages/assignment/src/contexts/theme/ThemeProvider.tsx @@ -0,0 +1,25 @@ +import React, { useState, ReactNode } from 'react' +import { ThemeContext, ThemeContextType } from './ThemeContext' + +interface ThemeProviderProps { + children: ReactNode +} + +export const ThemeProvider: React.FC = ({ children }) => { + const [theme, setTheme] = useState('light') + + const toggleTheme = () => { + setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light')) + } + + const contextValue: ThemeContextType = { + theme, + toggleTheme, + } + + return ( + + {children} + + ) +} diff --git a/packages/assignment/src/contexts/theme/index.ts b/packages/assignment/src/contexts/theme/index.ts new file mode 100644 index 00000000..54e05fc4 --- /dev/null +++ b/packages/assignment/src/contexts/theme/index.ts @@ -0,0 +1,3 @@ +export * from './ThemeContext' +export * from './ThemeProvider' +export * from './useThemeContext' diff --git a/packages/assignment/src/contexts/theme/useThemeContext.ts b/packages/assignment/src/contexts/theme/useThemeContext.ts new file mode 100644 index 00000000..dbe68002 --- /dev/null +++ b/packages/assignment/src/contexts/theme/useThemeContext.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react' +import { ThemeContext } from './ThemeContext' + +export const useThemeContext = () => { + const context = useContext(ThemeContext) + if (context === undefined) { + throw new Error('useThemeContext must be used within an ThemeProvider') + } + return context +} diff --git a/packages/assignment/src/contexts/user/UserContext.ts b/packages/assignment/src/contexts/user/UserContext.ts new file mode 100644 index 00000000..46d36ef0 --- /dev/null +++ b/packages/assignment/src/contexts/user/UserContext.ts @@ -0,0 +1,10 @@ +import { createContext } from 'react' +import { User } from '../../types' + +export interface UserContextType { + user: User | null + login: (email: string, password: string) => void + logout: () => void +} + +export const UserContext = createContext(undefined) diff --git a/packages/assignment/src/contexts/user/UserProvider.tsx b/packages/assignment/src/contexts/user/UserProvider.tsx new file mode 100644 index 00000000..d02cacd7 --- /dev/null +++ b/packages/assignment/src/contexts/user/UserProvider.tsx @@ -0,0 +1,33 @@ +import React, { useState, ReactNode } from 'react' +import { UserContext, UserContextType } from './UserContext' +import { useNotificationContext } from '../notification' +import { User } from '../../types' + +interface UserProviderProps { + children: ReactNode +} + +export const UserProvider: React.FC = ({ children }) => { + const [user, setUser] = useState(null) + const { addNotification } = useNotificationContext() + + const login = (email: string) => { + setUser({ id: 1, name: '홍길동', email }) + addNotification('성공적으로 로그인되었습니다', 'success') + } + + const logout = () => { + setUser(null) + addNotification('로그아웃되었습니다', 'info') + } + + const contextValue: UserContextType = { + user, + login, + logout, + } + + return ( + {children} + ) +} diff --git a/packages/assignment/src/contexts/user/index.ts b/packages/assignment/src/contexts/user/index.ts new file mode 100644 index 00000000..1c5f8af0 --- /dev/null +++ b/packages/assignment/src/contexts/user/index.ts @@ -0,0 +1,3 @@ +export * from './UserContext' +export * from './UserProvider' +export * from './useUserContext' diff --git a/packages/assignment/src/contexts/user/useUserContext.ts b/packages/assignment/src/contexts/user/useUserContext.ts new file mode 100644 index 00000000..32c89b9e --- /dev/null +++ b/packages/assignment/src/contexts/user/useUserContext.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react' +import { UserContext } from './UserContext' + +export const useUserContext = () => { + const context = useContext(UserContext) + if (context === undefined) { + throw new Error('useUserContext must be used within an UserProvider') + } + return context +} From e41becfcd7e3c05b14afcddf2f501d208a5703fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B1=84=EC=98=88=EB=A6=B0?= Date: Thu, 10 Oct 2024 01:48:37 +0900 Subject: [PATCH 11/11] =?UTF-8?q?refactor:=20=EC=8B=AC=ED=99=94=20?= =?UTF-8?q?=EA=B3=BC=EC=A0=9C=20=EC=99=84=EB=A3=8C=20-=20memo,=20useMemo,?= =?UTF-8?q?=20useCallback=20=EC=A0=81=EC=9A=A9=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EB=A6=AC=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=A7=81=20=EB=B0=A9=EC=A7=80=20(#3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 컴포넌트에 memo 적용 - context 변경으로 상위 컴포넌트가 리렌더링 되는 경우, 불필요한 리렌더링 방지 * refactor: UserProvider 최적화 적용 - notification context 의 변경이 있을 때, `useUserContext` 를 사용하는 컴포넌트가 리렌더링이 되는 문제 개선 (UserProvider 에서 `useNotificationContext` 를 사용하기 때문) - NotificationProvider 의 `addNotification` 에 `useCallback` 적용 - UserProvider의 context value 에 `useMemo`, 그 외 함수에 모두 `useCallback` 적용 * refactor: 상품 리스트 생성 함수에 useMemo 적용 --------- Co-authored-by: chyerin --- packages/assignment/src/App.tsx | 5 ++-- .../assignment/src/components/ComplexForm.tsx | 5 ++-- packages/assignment/src/components/Header.tsx | 4 ++- .../assignment/src/components/ItemList.tsx | 4 ++- .../src/components/NotificationSystem.tsx | 4 ++- .../notification/NotificationProvider.tsx | 20 ++++++++----- .../src/contexts/user/UserProvider.tsx | 29 ++++++++++++------- 7 files changed, 45 insertions(+), 26 deletions(-) diff --git a/packages/assignment/src/App.tsx b/packages/assignment/src/App.tsx index f5fc9572..a5a4ead6 100644 --- a/packages/assignment/src/App.tsx +++ b/packages/assignment/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React from 'react' import { generateItems } from './utils' import { ThemeProvider, @@ -6,13 +6,14 @@ import { NotificationProvider, useThemeContext, } from './contexts' +import { useMemo } from './@lib' // components import { ItemList, Header, ComplexForm, NotificationSystem } from './components' const AppContent: React.FC = () => { const { theme } = useThemeContext() - const [items] = useState(generateItems(10000)) + const items = useMemo(() => generateItems(10000), []) return (
{ +export const ComplexForm: React.FC = memo(() => { renderLog('ComplexForm rendered') const { addNotification } = useNotificationContext() const [formData, setFormData] = useState({ @@ -84,4 +85,4 @@ export const ComplexForm: React.FC = () => {
) -} +}) diff --git a/packages/assignment/src/components/Header.tsx b/packages/assignment/src/components/Header.tsx index 44d88269..e32f6a21 100644 --- a/packages/assignment/src/components/Header.tsx +++ b/packages/assignment/src/components/Header.tsx @@ -1,8 +1,9 @@ import { renderLog } from '../utils' import { useThemeContext, useUserContext } from '../contexts' +import { memo } from '../@lib' // Header 컴포넌트 -export const Header: React.FC = () => { +export const Header: React.FC = memo(() => { renderLog('Header rendered') const {theme,toggleTheme} = useThemeContext() const {user, login, logout} = useUserContext() @@ -46,3 +47,4 @@ export const Header: React.FC = () => {
) } +) \ No newline at end of file diff --git a/packages/assignment/src/components/ItemList.tsx b/packages/assignment/src/components/ItemList.tsx index 92e1a27b..67491187 100644 --- a/packages/assignment/src/components/ItemList.tsx +++ b/packages/assignment/src/components/ItemList.tsx @@ -1,6 +1,7 @@ import { useState } from 'react' import { renderLog } from '../utils' import { useThemeContext } from '../contexts' +import { memo } from '../@lib' interface Item { id: number @@ -9,7 +10,7 @@ interface Item { price: number } -export const ItemList: React.FC<{ items: Item[] }> = ({ items }) => { +export const ItemList: React.FC<{ items: Item[] }> = memo(({ items }) => { renderLog('ItemList rendered') const [filter, setFilter] = useState('') const { theme } = useThemeContext() @@ -50,3 +51,4 @@ export const ItemList: React.FC<{ items: Item[] }> = ({ items }) => { ) } +) \ No newline at end of file diff --git a/packages/assignment/src/components/NotificationSystem.tsx b/packages/assignment/src/components/NotificationSystem.tsx index 351a553c..bbe6d832 100644 --- a/packages/assignment/src/components/NotificationSystem.tsx +++ b/packages/assignment/src/components/NotificationSystem.tsx @@ -1,7 +1,8 @@ import { renderLog } from '../utils' import { useNotificationContext } from '../contexts' +import { memo } from '../@lib' -export const NotificationSystem: React.FC = () => { +export const NotificationSystem: React.FC = memo(() => { renderLog('NotificationSystem rendered') const { notifications, removeNotification } = useNotificationContext() @@ -32,3 +33,4 @@ export const NotificationSystem: React.FC = () => { ) } +) \ No newline at end of file diff --git a/packages/assignment/src/contexts/notification/NotificationProvider.tsx b/packages/assignment/src/contexts/notification/NotificationProvider.tsx index c60d4e0a..edf2ee28 100644 --- a/packages/assignment/src/contexts/notification/NotificationProvider.tsx +++ b/packages/assignment/src/contexts/notification/NotificationProvider.tsx @@ -4,6 +4,7 @@ import { NotificationContextType, } from './NotificationContext' import { Notification } from '../../types' +import { useCallback } from '../../@lib' interface NotificationProviderProps { children: ReactNode @@ -14,14 +15,17 @@ export const NotificationProvider: React.FC = ({ }) => { const [notifications, setNotifications] = useState([]) - const addNotification = (message: string, type: Notification['type']) => { - const newNotification: Notification = { - id: Date.now(), - message, - type, - } - setNotifications((prev) => [...prev, newNotification]) - } + const addNotification = useCallback( + (message: string, type: Notification['type']) => { + const newNotification: Notification = { + id: Date.now(), + message, + type, + } + setNotifications((prev) => [...prev, newNotification]) + }, + [] + ) const removeNotification = (id: number) => { setNotifications((prev) => diff --git a/packages/assignment/src/contexts/user/UserProvider.tsx b/packages/assignment/src/contexts/user/UserProvider.tsx index d02cacd7..ba590461 100644 --- a/packages/assignment/src/contexts/user/UserProvider.tsx +++ b/packages/assignment/src/contexts/user/UserProvider.tsx @@ -2,6 +2,7 @@ import React, { useState, ReactNode } from 'react' import { UserContext, UserContextType } from './UserContext' import { useNotificationContext } from '../notification' import { User } from '../../types' +import { useCallback, useMemo } from '../../@lib' interface UserProviderProps { children: ReactNode @@ -11,21 +12,27 @@ export const UserProvider: React.FC = ({ children }) => { const [user, setUser] = useState(null) const { addNotification } = useNotificationContext() - const login = (email: string) => { - setUser({ id: 1, name: '홍길동', email }) - addNotification('성공적으로 로그인되었습니다', 'success') - } + const login = useCallback( + (email: string) => { + setUser({ id: 1, name: '홍길동', email }) + addNotification('성공적으로 로그인되었습니다', 'success') + }, + [addNotification] + ) - const logout = () => { + const logout = useCallback(() => { setUser(null) addNotification('로그아웃되었습니다', 'info') - } + }, [addNotification]) - const contextValue: UserContextType = { - user, - login, - logout, - } + const contextValue: UserContextType = useMemo( + () => ({ + user, + login, + logout, + }), + [user, login, logout] + ) return ( {children}