From c7c088e475e224639ea173acc1deaafbde0d8007 Mon Sep 17 00:00:00 2001 From: JaeHyun Bang Date: Mon, 30 Dec 2024 15:48:45 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat=20:=20=EC=96=95=EC=9D=80=20=EB=B3=B5?= =?UTF-8?q?=EC=82=AC(shallowEquals)=20=ED=95=A8=EC=88=98=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/@lib/equalities/shallowEquals.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/@lib/equalities/shallowEquals.ts b/src/@lib/equalities/shallowEquals.ts index 56bf6660..f90e8b12 100644 --- a/src/@lib/equalities/shallowEquals.ts +++ b/src/@lib/equalities/shallowEquals.ts @@ -1,3 +1,21 @@ export function shallowEquals(objA: T, objB: T): boolean { - return objA === objB; + if (objA === objB) return true; + if ( + typeof objA !== "object" || + typeof objB !== "object" || + objA === null || + objB === null + ) + return false; + const keyB = Object.keys(objB) as (keyof T)[]; + const keyA = Object.keys(objA) as (keyof T)[]; + if (keyB.length !== keyA.length) return false; + for (const key of keyA) { + if ( + !Object.prototype.hasOwnProperty.call(objB, key) || + objA[key] !== objB[key] + ) + return false; + } + return true; } From 51704e2db1bf007cf6f606f77a9c90ee25b1c521 Mon Sep 17 00:00:00 2001 From: JaeHyun Bang Date: Mon, 30 Dec 2024 17:15:21 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat=20:=20=EA=B9=8A=EC=9D=80=20=EB=B9=84?= =?UTF-8?q?=EA=B5=90(deepEquals)=20=ED=95=A8=EC=88=98=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/@lib/equalities/deepEquals.ts | 33 ++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/@lib/equalities/deepEquals.ts b/src/@lib/equalities/deepEquals.ts index af583d11..3adb708c 100644 --- a/src/@lib/equalities/deepEquals.ts +++ b/src/@lib/equalities/deepEquals.ts @@ -1,3 +1,34 @@ export function deepEquals(objA: T, objB: T): boolean { - return objA === objB; + if (objA === objB) return true; + if ( + typeof objA !== "object" || + typeof objB !== "object" || + objA === null || + objB === null + ) { + return false; + } + + if (Array.isArray(objA) && Array.isArray(objB)) { + if (objA.length !== objB.length) { + return false; + } + return objA.every((item, index) => deepEquals(item, objB[index])); + } + + if (Array.isArray(objA) || Array.isArray(objB)) { + return false; + } + + const keysA = Object.keys(objA) as (keyof T)[]; + const keysB = Object.keys(objB) as (keyof T)[]; + + if (keysA.length !== keysB.length) return false; + for (const key of keysA) { + if (!keysB.includes(key) || !deepEquals(objA[key], objB[key])) { + return false; + } + } + + return true; } From 5ace5ff625be84301b2f64e2258a0fbb5ece5fe5 Mon Sep 17 00:00:00 2001 From: JaeHyun Bang Date: Thu, 2 Jan 2025 09:37:38 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat=20:=20custom=20useRef=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/@lib/hooks/useRef.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/@lib/hooks/useRef.ts b/src/@lib/hooks/useRef.ts index 2dc9e83f..9672e0c3 100644 --- a/src/@lib/hooks/useRef.ts +++ b/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 }; + const [ref] = useState({ current: initialValue }); + return ref; } From d7805684aaea2d9e8d41a13c1065e10384603e95 Mon Sep 17 00:00:00 2001 From: JaeHyun Bang Date: Thu, 2 Jan 2025 09:44:14 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat=20:=20custom=20useCallback=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/@lib/hooks/useCallback.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/@lib/hooks/useCallback.ts b/src/@lib/hooks/useCallback.ts index e71e647d..d237ae6d 100644 --- a/src/@lib/hooks/useCallback.ts +++ b/src/@lib/hooks/useCallback.ts @@ -1,10 +1,11 @@ -/* eslint-disable @typescript-eslint/no-unused-vars,@typescript-eslint/no-unsafe-function-type */ -import { DependencyList } from "react"; +/* eslint-disable @typescript-eslint/no-unsafe-function-type */ +import { DependencyList, useMemo } from "react"; export function useCallback( factory: T, _deps: DependencyList, ) { // 직접 작성한 useMemo를 통해서 만들어보세요. - return factory as T; + const callbackMemo = useMemo(() => factory, _deps); + return callbackMemo; } From 547a49a3928f8fb2bbff13d0236f4bfa35a88c85 Mon Sep 17 00:00:00 2001 From: JaeHyun Bang Date: Thu, 2 Jan 2025 11:39:28 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat=20:=20custom=20useMemo=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/@lib/hooks/useMemo.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/@lib/hooks/useMemo.ts b/src/@lib/hooks/useMemo.ts index 95930d69..de5ed4c6 100644 --- a/src/@lib/hooks/useMemo.ts +++ b/src/@lib/hooks/useMemo.ts @@ -1,6 +1,6 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { DependencyList } from "react"; import { shallowEquals } from "../equalities"; +import { useRef } from "./useRef"; export function useMemo( factory: () => T, @@ -8,5 +8,13 @@ export function useMemo( _equals = shallowEquals, ): T { // 직접 작성한 useRef를 통해서 만들어보세요. - return factory(); + const value = useRef(null); + const deps = useRef(_deps); + + if (value.current === null || !_equals(deps.current, _deps)) { + value.current = factory(); + deps.current = _deps; + } + + return value.current; } From 08f782bf58d16db2dc59e5a2779750fbadf4c50d Mon Sep 17 00:00:00 2001 From: JaeHyun Bang Date: Thu, 2 Jan 2025 11:42:36 +0900 Subject: [PATCH 6/9] =?UTF-8?q?feat=20:=20memo=20HOC=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/@lib/hocs/memo.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/@lib/hocs/memo.ts b/src/@lib/hocs/memo.ts index d43559d0..4ec2b6ea 100644 --- a/src/@lib/hocs/memo.ts +++ b/src/@lib/hocs/memo.ts @@ -1,10 +1,19 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { shallowEquals } from "../equalities"; import { ComponentType } from "react"; +import { useRef } from "../hooks"; +import React from "react"; export function memo

( Component: ComponentType

, - _equals = shallowEquals, + _equals = shallowEquals ) { - return Component; + const memoComponent = (props: P) => { + const prevElement = useRef

(null); + if (prevElement.current === null || !_equals(prevElement.current, props)) { + prevElement.current = props; + return React.createElement(Component, prevElement.current); + } + }; + return memoComponent; } From 5a876adfd8d2ed76da3abd736c2ca6a1dc400b17 Mon Sep 17 00:00:00 2001 From: JaeHyun Bang Date: Fri, 3 Jan 2025 08:01:14 +0900 Subject: [PATCH 7/9] =?UTF-8?q?=EC=9E=84=EC=8B=9C=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .prettierrc | 3 + package-lock.json | 18 ++ package.json | 6 +- src/@context/notiContext.tsx | 47 ++++ src/@context/themeContext.tsx | 36 +++ src/@context/userContext.tsx | 37 +++ src/App.tsx | 339 +++----------------------- src/components/ComplexForm.tsx | 88 +++++++ src/components/Header.tsx | 47 ++++ src/components/ItemList.tsx | 63 +++++ src/components/NotificationSystem.tsx | 35 +++ src/types/Context.ts | 24 ++ src/types/Item.ts | 6 + src/types/Notification.ts | 5 + src/types/Theme.ts | 4 + src/types/User.ts | 5 + src/utils/AppContext.ts | 13 + 17 files changed, 473 insertions(+), 303 deletions(-) create mode 100644 .prettierrc create mode 100644 src/@context/notiContext.tsx create mode 100644 src/@context/themeContext.tsx create mode 100644 src/@context/userContext.tsx create mode 100644 src/components/ComplexForm.tsx create mode 100644 src/components/Header.tsx create mode 100644 src/components/ItemList.tsx create mode 100644 src/components/NotificationSystem.tsx create mode 100644 src/types/Context.ts create mode 100644 src/types/Item.ts create mode 100644 src/types/Notification.ts create mode 100644 src/types/Theme.ts create mode 100644 src/types/User.ts create mode 100644 src/utils/AppContext.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..36b35631 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "trailingComma": "none" +} diff --git a/package-lock.json b/package-lock.json index d2ad619c..9d67a135 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", + "@types/node": "^22.10.3", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react-swc": "^3.5.0", @@ -1418,6 +1419,16 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/node": { + "version": "22.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.3.tgz", + "integrity": "sha512-DifAyw4BkrufCILvD3ucnuN8eydUfc/C1GlyrnI+LK6543w5/L3VeVgf05o3B4fqSXP1dKYLOZsKfutpxPzZrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -4671,6 +4682,13 @@ "typescript": ">=4.8.4 <5.8.0" } }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/package.json b/package.json index 9cb22edf..2519dcb1 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "lint-staged": { "*.{js,jsx,ts,tsx}": [ "tsc --noEmit", + "tsc --noEmit --jsx react-jsx", "prettier --write", "eslint --fix" ] @@ -34,15 +35,16 @@ "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", + "@types/node": "^22.10.3", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react-swc": "^3.5.0", "@vitest/coverage-v8": "^2.1.2", "eslint": "^9.9.0", - "eslint-plugin-react-hooks": "^5.1.0-rc.0", - "eslint-plugin-react-refresh": "^0.4.9", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", "globals": "^15.9.0", "husky": "^9.1.7", "jsdom": "^25.0.1", diff --git a/src/@context/notiContext.tsx b/src/@context/notiContext.tsx new file mode 100644 index 00000000..f7d1c33a --- /dev/null +++ b/src/@context/notiContext.tsx @@ -0,0 +1,47 @@ +import { createContext, ReactNode, useContext, useState } from "react"; +import { INotificationContext } from "../types/Context"; +import { useCallback, useMemo } from "../@lib"; +import { INotification } from "../types/Notification"; + +export const NotiContext = createContext( + undefined +); + +export const useNotiContext = () => { + const context = useContext(NotiContext); + if (context === undefined) { + throw new Error("useNotiContext must be used within an AppProvider"); + } + return context; +}; + +export const NotiProvider = (children: ReactNode) => { + const [notifications, setNotifications] = useState([]); + + const addNotification = useCallback( + (message: string, type: INotification["type"]) => { + const newNotification: INotification = { + id: Date.now(), + message, + type + }; + setNotifications((prev) => [...prev, newNotification]); + }, + [] + ); + + const removeNotification = useCallback((id: number) => { + setNotifications((prev) => + prev.filter((notification) => notification.id !== id) + ); + }, []); + + const contextValue = useMemo( + () => ({ notifications, addNotification, removeNotification }), + [notifications, addNotification, removeNotification] + ); + + return ( + {children} + ); +}; diff --git a/src/@context/themeContext.tsx b/src/@context/themeContext.tsx new file mode 100644 index 00000000..e90bc36d --- /dev/null +++ b/src/@context/themeContext.tsx @@ -0,0 +1,36 @@ +import { createContext, useContext, useState } from "react"; +import { IThemeContext } from "../types/Theme"; +import { IContextProps } from "../types/Context"; +import { useCallback, useMemo } from "../@lib"; + +export const ThemeContext = createContext(undefined); + +export const useThemeContext = () => { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error("useThemeContext must be used within an AppProvider"); + } + return context; +}; + +// toggleTheme: () => void; +export const ThemeProvider = ({ children }: IContextProps) => { + const [theme, setTheme] = useState("light"); + const toggleTheme = useCallback(() => { + setTheme((prev) => (prev === "light" ? "dark" : "light")); + }, []); + + const contextValue = useMemo( + () => ({ + theme, + toggleTheme + }), + [theme, toggleTheme] + ); + + return ( + + {children} + + ); +}; diff --git a/src/@context/userContext.tsx b/src/@context/userContext.tsx new file mode 100644 index 00000000..20dec872 --- /dev/null +++ b/src/@context/userContext.tsx @@ -0,0 +1,37 @@ +import { createContext, useCallback, useContext, useState } from "react"; +import { IContextProps, IUserContext } from "../types/Context"; +import { useMemo } from "../@lib"; +import { IUser } from "../types/User"; +import { useNotiContext } from "./notiContext"; + +export const UserContext = createContext(undefined); +export const useUserContext = () => { + const context = useContext(UserContext); + if (context === undefined) { + throw new Error("useNotiContext must be used within an AppProvider"); + } + return context; +}; + +export const UserProvider = ({ children }: IContextProps) => { + const [user, setUser] = useState(null); + const { addNotification } = useNotiContext(); + + const login = useCallback((email: string) => { + setUser({ id: 1, name: "홍길동", email }); + addNotification("성공적으로 로그인되었습니다", "success"); + }, []); + const logout = useCallback(() => {}, []); + + const contextValue = useMemo( + () => ({ + user, + login, + logout + }), + [user, login, logout] + ); + return ( + {children} + ); +}; diff --git a/src/App.tsx b/src/App.tsx index debd6454..8e75091d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,277 +1,20 @@ -import React, { useState, createContext, useContext } from "react"; -import { generateItems, renderLog } from "./utils"; - -// 타입 정의 -interface Item { - id: number; - name: string; - category: string; - price: number; -} - -interface User { - id: number; - name: string; - email: string; -} - -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"); - }; - - return ( -

-
-

샘플 애플리케이션

-
- - {user ? ( -
- {user.name}님 환영합니다! - -
- ) : ( - - )} -
-
-
- ); -}; - -// ItemList 컴포넌트 -export const ItemList: React.FC<{ - items: Item[]; - onAddItemsClick: () => void; -}> = ({ items, onAddItemsClick }) => { - 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 totalPrice = filteredItems.reduce((sum, item) => sum + item.price, 0); - - const averagePrice = Math.round(totalPrice / filteredItems.length) || 0; - - return ( -
-
-

상품 목록

-
- -
-
- setFilter(e.target.value)} - className="w-full p-2 mb-4 border border-gray-300 rounded text-black" - /> -
    -
  • 검색결과: {filteredItems.length.toLocaleString()}개
  • -
  • 전체가격: {totalPrice.toLocaleString()}원
  • -
  • 평균가격: {averagePrice.toLocaleString()}원
  • -
-
    - {filteredItems.map((item, index) => ( -
  • - {item.name} - {item.category} - {item.price.toLocaleString()}원 -
  • - ))} -
-
- ); -}; - -// 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} - -
- ))} -
- ); -}; +import React, { useState } from "react"; +import { generateItems } from "./utils"; +import { IAppContext, INotificationContext } from "./types/Context"; +import { INotification } from "./types/Notification"; +import { AppContext } from "./utils/AppContext"; +import { Header } from "./components/Header"; +import { NotificationSystem } from "./components/NotificationSystem"; +import { ComplexForm } from "./components/ComplexForm"; +import { ItemList } from "./components/ItemList"; +import { UserProvider } from "./@context/userContext"; +import { NotiContext } from "./@context/notiContext"; // 메인 App 컴포넌트 const App: React.FC = () => { const [theme, setTheme] = useState("light"); const [items, setItems] = useState(generateItems(1000)); - const [user, setUser] = useState(null); - const [notifications, setNotifications] = useState([]); + const [notifications, setNotifications] = useState([]); const toggleTheme = () => { setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light")); @@ -280,44 +23,34 @@ const App: React.FC = () => { const addItems = () => { setItems((prevItems) => [ ...prevItems, - ...generateItems(1000, prevItems.length), + ...generateItems(1000, prevItems.length) ]); }; - 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 = { + const addNotification = (message: string, type: INotification["type"]) => { + const newNotification: INotification = { id: Date.now(), message, - type, + type }; setNotifications((prev) => [...prev, newNotification]); }; const removeNotification = (id: number) => { setNotifications((prev) => - prev.filter((notification) => notification.id !== id), + prev.filter((notification) => notification.id !== id) ); }; - const contextValue: AppContextType = { - theme, - toggleTheme, - user, - login, - logout, + const notiContextValue: INotificationContext = { notifications, addNotification, - removeNotification, + removeNotification + }; + + const contextValue: IAppContext = { + theme, + toggleTheme }; return ( @@ -325,18 +58,22 @@ const App: React.FC = () => {
-
-
-
-
- -
-
- + + +
+ +
+
+
+ +
+
+ +
-
- + +
); diff --git a/src/components/ComplexForm.tsx b/src/components/ComplexForm.tsx new file mode 100644 index 00000000..f637034e --- /dev/null +++ b/src/components/ComplexForm.tsx @@ -0,0 +1,88 @@ +import { useState } from "react"; +import { renderLog } from "../utils"; +import { useNotiContext } from "../@context/notiContext"; +import { memo } from "../@lib"; + +export const ComplexForm: React.FC = memo(() => { + renderLog("ComplexForm rendered"); + const { addNotification } = useNotiContext(); + 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/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 00000000..b065e23f --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,47 @@ +import { memo } from "../@lib"; +import { renderLog } from "../utils"; +import { useAppContext } from "../utils/AppContext"; + +export const Header: React.FC = memo(() => { + renderLog("Header rendered"); + const { theme, toggleTheme, user, login, logout } = useAppContext(); + + const handleLogin = () => { + // 실제 애플리케이션에서는 사용자 입력을 받아야 합니다. + login("user@example.com", "password"); + }; + + return ( +
+
+

샘플 애플리케이션

+
+ + {user ? ( +
+ {user.name}님 환영합니다! + +
+ ) : ( + + )} +
+
+
+ ); +}); diff --git a/src/components/ItemList.tsx b/src/components/ItemList.tsx new file mode 100644 index 00000000..ec746879 --- /dev/null +++ b/src/components/ItemList.tsx @@ -0,0 +1,63 @@ +import { useState } from "react"; +import { IItem } from "../types/Item"; +import { renderLog } from "../utils"; +import { useAppContext } from "../utils/AppContext"; +import { memo } from "../@lib"; + +export const ItemList: React.FC<{ + items: IItem[]; + onAddItemsClick: () => void; +}> = memo(({ items, onAddItemsClick }) => { + 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 totalPrice = filteredItems.reduce((sum, item) => sum + item.price, 0); + + const averagePrice = Math.round(totalPrice / filteredItems.length) || 0; + + return ( +
+
+

상품 목록

+
+ +
+
+ setFilter(e.target.value)} + className="w-full p-2 mb-4 border border-gray-300 rounded text-black" + /> +
    +
  • 검색결과: {filteredItems.length.toLocaleString()}개
  • +
  • 전체가격: {totalPrice.toLocaleString()}원
  • +
  • 평균가격: {averagePrice.toLocaleString()}원
  • +
+
    + {filteredItems.map((item, index) => ( +
  • + {item.name} - {item.category} - {item.price.toLocaleString()}원 +
  • + ))} +
+
+ ); +}); diff --git a/src/components/NotificationSystem.tsx b/src/components/NotificationSystem.tsx new file mode 100644 index 00000000..00672f97 --- /dev/null +++ b/src/components/NotificationSystem.tsx @@ -0,0 +1,35 @@ +import { useNotiContext } from "../@context/notiContext"; +import { memo } from "../@lib"; +import { renderLog } from "../utils"; + +export const NotificationSystem: React.FC = memo(() => { + renderLog("NotificationSystem rendered"); + const { notifications, removeNotification } = useNotiContext(); + + return ( +
+ {notifications.map((notification) => ( +
+ {notification.message} + +
+ ))} +
+ ); +}); diff --git a/src/types/Context.ts b/src/types/Context.ts new file mode 100644 index 00000000..4446d170 --- /dev/null +++ b/src/types/Context.ts @@ -0,0 +1,24 @@ +import { IUser } from "./User"; +import { INotification } from "./Notification"; +import { ReactNode } from "react"; + +export interface IAppContext { + theme: string; + toggleTheme: () => void; +} + +export interface INotificationContext { + notifications: INotification[]; + addNotification: (message: string, type: INotification["type"]) => void; + removeNotification: (id: number) => void; +} + +export interface IUserContext { + user: IUser | null; + login: (email: string, password: string) => void; + logout: () => void; +} + +export interface IContextProps { + children: ReactNode; +} diff --git a/src/types/Item.ts b/src/types/Item.ts new file mode 100644 index 00000000..d57bdee8 --- /dev/null +++ b/src/types/Item.ts @@ -0,0 +1,6 @@ +export interface IItem { + id: number; + name: string; + category: string; + price: number; +} diff --git a/src/types/Notification.ts b/src/types/Notification.ts new file mode 100644 index 00000000..2be987fe --- /dev/null +++ b/src/types/Notification.ts @@ -0,0 +1,5 @@ +export interface INotification { + id: number; + message: string; + type: "info" | "success" | "warning" | "error"; +} diff --git a/src/types/Theme.ts b/src/types/Theme.ts new file mode 100644 index 00000000..74c30a14 --- /dev/null +++ b/src/types/Theme.ts @@ -0,0 +1,4 @@ +export interface IThemeContext { + theme: string; + toggleTheme: () => void; +} diff --git a/src/types/User.ts b/src/types/User.ts new file mode 100644 index 00000000..52f69b1c --- /dev/null +++ b/src/types/User.ts @@ -0,0 +1,5 @@ +export interface IUser { + id: number; + name: string; + email: string; +} diff --git a/src/utils/AppContext.ts b/src/utils/AppContext.ts new file mode 100644 index 00000000..dd483842 --- /dev/null +++ b/src/utils/AppContext.ts @@ -0,0 +1,13 @@ +import { createContext, useContext } from "react"; +import { IAppContext } from "../types/Context"; + +export const AppContext = createContext(undefined); + +// 커스텀 훅: useAppContext +export const useAppContext = () => { + const context = useContext(AppContext); + if (context === undefined) { + throw new Error("useAppContext must be used within an AppProvider"); + } + return context; +}; From c80e890bf869baf5d570a26788f9e950c378513d Mon Sep 17 00:00:00 2001 From: JaeHyun Bang Date: Fri, 3 Jan 2025 10:03:14 +0900 Subject: [PATCH 8/9] =?UTF-8?q?=EB=A6=AC=EB=A0=8C=EB=8D=94=EB=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/@context/notiContext.tsx | 2 +- src/@context/themeContext.tsx | 6 ++- src/App.tsx | 74 +++++++---------------------------- src/components/Header.tsx | 7 +++- src/components/ItemList.tsx | 45 +++++++++++++-------- src/utils/AppContext.ts | 13 ------ 6 files changed, 55 insertions(+), 92 deletions(-) delete mode 100644 src/utils/AppContext.ts diff --git a/src/@context/notiContext.tsx b/src/@context/notiContext.tsx index f7d1c33a..845d1231 100644 --- a/src/@context/notiContext.tsx +++ b/src/@context/notiContext.tsx @@ -15,7 +15,7 @@ export const useNotiContext = () => { return context; }; -export const NotiProvider = (children: ReactNode) => { +export const NotiProvider = ({ children }: { children: ReactNode }) => { const [notifications, setNotifications] = useState([]); const addNotification = useCallback( diff --git a/src/@context/themeContext.tsx b/src/@context/themeContext.tsx index e90bc36d..599c84fc 100644 --- a/src/@context/themeContext.tsx +++ b/src/@context/themeContext.tsx @@ -30,7 +30,11 @@ export const ThemeProvider = ({ children }: IContextProps) => { return ( - {children} +
+ {children} +
); }; diff --git a/src/App.tsx b/src/App.tsx index 8e75091d..4d98752c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,71 +1,27 @@ -import React, { useState } from "react"; -import { generateItems } from "./utils"; -import { IAppContext, INotificationContext } from "./types/Context"; -import { INotification } from "./types/Notification"; -import { AppContext } from "./utils/AppContext"; +import React from "react"; +// import { generateItems } from "./utils"; +// import { IAppContext, INotificationContext } from "./types/Context"; +// import { INotification } from "./types/Notification"; +// import { AppContext } from "./utils/AppContext"; import { Header } from "./components/Header"; import { NotificationSystem } from "./components/NotificationSystem"; import { ComplexForm } from "./components/ComplexForm"; import { ItemList } from "./components/ItemList"; import { UserProvider } from "./@context/userContext"; -import { NotiContext } from "./@context/notiContext"; +import { NotiProvider } from "./@context/notiContext"; +import { ThemeProvider } from "./@context/themeContext"; // 메인 App 컴포넌트 const App: React.FC = () => { - const [theme, setTheme] = useState("light"); - const [items, setItems] = useState(generateItems(1000)); - const [notifications, setNotifications] = useState([]); - - const toggleTheme = () => { - setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light")); - }; - - const addItems = () => { - setItems((prevItems) => [ - ...prevItems, - ...generateItems(1000, prevItems.length) - ]); - }; - - const addNotification = (message: string, type: INotification["type"]) => { - const newNotification: INotification = { - id: Date.now(), - message, - type - }; - setNotifications((prev) => [...prev, newNotification]); - }; - - const removeNotification = (id: number) => { - setNotifications((prev) => - prev.filter((notification) => notification.id !== id) - ); - }; - - const notiContextValue: INotificationContext = { - notifications, - addNotification, - removeNotification - }; - - const contextValue: IAppContext = { - theme, - toggleTheme - }; - return ( - -
- - -
- + + + +
- +
@@ -73,9 +29,9 @@ const App: React.FC = () => {
- -
- + + + ); }; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index b065e23f..880b3d10 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,10 +1,13 @@ import { memo } from "../@lib"; import { renderLog } from "../utils"; -import { useAppContext } from "../utils/AppContext"; +import { useThemeContext } from "../@context/themeContext"; +import { useUserContext } from "../@context/userContext"; export const Header: React.FC = memo(() => { renderLog("Header rendered"); - const { theme, toggleTheme, user, login, logout } = useAppContext(); + + const { theme, toggleTheme } = useThemeContext(); + const { user, login, logout } = useUserContext(); const handleLogin = () => { // 실제 애플리케이션에서는 사용자 입력을 받아야 합니다. diff --git a/src/components/ItemList.tsx b/src/components/ItemList.tsx index ec746879..3e78ea26 100644 --- a/src/components/ItemList.tsx +++ b/src/components/ItemList.tsx @@ -1,26 +1,39 @@ import { useState } from "react"; -import { IItem } from "../types/Item"; -import { renderLog } from "../utils"; -import { useAppContext } from "../utils/AppContext"; -import { memo } from "../@lib"; +import { generateItems, renderLog } from "../utils"; +import { memo, useCallback, useMemo } from "../@lib"; +import { useThemeContext } from "../@context/themeContext"; -export const ItemList: React.FC<{ - items: IItem[]; - onAddItemsClick: () => void; -}> = memo(({ items, onAddItemsClick }) => { +export const ItemList: React.FC = memo(() => { 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 [items, setItems] = useState(generateItems(1000)); + const { theme } = useThemeContext(); + const addItems = useCallback(() => { + setItems((prevItems) => [ + ...prevItems, + ...generateItems(1000, prevItems.length) + ]); + }, []); + const filteredItems = useMemo( + () => + items.filter( + (item) => + item.name.toLowerCase().includes(filter.toLowerCase()) || + item.category.toLowerCase().includes(filter.toLowerCase()) + ), + [items, filter] ); - const totalPrice = filteredItems.reduce((sum, item) => sum + item.price, 0); + const totalPrice = useMemo( + () => filteredItems.reduce((sum, item) => sum + item.price, 0), + [filteredItems] + ); - const averagePrice = Math.round(totalPrice / filteredItems.length) || 0; + const averagePrice = useMemo( + () => Math.round(totalPrice / filteredItems.length) || 0, + [totalPrice, filteredItems.length] + ); return (
@@ -30,7 +43,7 @@ export const ItemList: React.FC<{ diff --git a/src/utils/AppContext.ts b/src/utils/AppContext.ts deleted file mode 100644 index dd483842..00000000 --- a/src/utils/AppContext.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createContext, useContext } from "react"; -import { IAppContext } from "../types/Context"; - -export const AppContext = createContext(undefined); - -// 커스텀 훅: useAppContext -export const useAppContext = () => { - const context = useContext(AppContext); - if (context === undefined) { - throw new Error("useAppContext must be used within an AppProvider"); - } - return context; -}; From 4860be25fa4c42fc399f867f7b067d21606d4e6d Mon Sep 17 00:00:00 2001 From: JaeHyun Bang Date: Fri, 3 Jan 2025 10:14:57 +0900 Subject: [PATCH 9/9] =?UTF-8?q?test=20:=20=EC=8B=AC=ED=99=94=EA=B3=BC?= =?UTF-8?q?=EC=A0=9C=20=ED=86=B5=EA=B3=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/@context/userContext.tsx | 6 +++++- src/components/ItemList.tsx | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/@context/userContext.tsx b/src/@context/userContext.tsx index 20dec872..af029ad8 100644 --- a/src/@context/userContext.tsx +++ b/src/@context/userContext.tsx @@ -21,7 +21,11 @@ export const UserProvider = ({ children }: IContextProps) => { setUser({ id: 1, name: "홍길동", email }); addNotification("성공적으로 로그인되었습니다", "success"); }, []); - const logout = useCallback(() => {}, []); + // const logout = useCallback(() => {}, []); + const logout = useCallback(() => { + setUser(null); + addNotification("로그아웃 되었습니다.", "success"); + }, [addNotification]); const contextValue = useMemo( () => ({ diff --git a/src/components/ItemList.tsx b/src/components/ItemList.tsx index 3e78ea26..4899ef5c 100644 --- a/src/components/ItemList.tsx +++ b/src/components/ItemList.tsx @@ -7,7 +7,7 @@ export const ItemList: React.FC = memo(() => { renderLog("ItemList rendered"); const [filter, setFilter] = useState(""); - const [items, setItems] = useState(generateItems(1000)); + const [items, setItems] = useState(() => generateItems(1000)); const { theme } = useThemeContext(); const addItems = useCallback(() => { setItems((prevItems) => [