diff --git a/src/@lib/equalities/deepEquals.ts b/src/@lib/equalities/deepEquals.ts index af583d11..62fa8742 100644 --- a/src/@lib/equalities/deepEquals.ts +++ b/src/@lib/equalities/deepEquals.ts @@ -1,3 +1,51 @@ export function deepEquals(objA: T, objB: T): boolean { - return objA === objB; + // 1. 기본 타입이거나 null인 경우 처리 + // primitive type 외에는 모두 Object를 상속받음. + // symbole은 deepEquals 시 비교 X -> 필요시 추후 구현 + // Q. 그냥 objA === objB로 비교해도 되지 않나? + if ( + typeof objA !== "object" || + objA === null || + typeof objB !== "object" || + objB === null + ) { + return objA === objB; + } + + // 2. 둘 다 객체인 경우: + // - 배열인지 확인 + // - 객체의 키 개수가 다른 경우 처리 + // - 재귀적으로 각 속성에 대해 deepEquals 호출 + if (Array.isArray(objA) && Array.isArray(objB)) { + if (objA.length !== objB.length) { + return false; + } + + for (let i = 0; i < objA.length; i++) { + if (!deepEquals(objA[i], objB[i])) { + return false; + } + } + } else { + // 두 객체의 키 개수가 다른 경우 + 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 ( + // general하게 사용되는 deepEquals이므로, Object.prototype.hasOwnProperty를 사용 (call로 매핑해줌으로써, 예외상황을 처리) + !Object.prototype.hasOwnProperty.call(objB, key) || + !deepEquals(objA[key], objB[key]) + ) { + return false; + } + } + } + + // 이 부분을 적절히 수정하세요. + return true; } diff --git a/src/@lib/equalities/shallowEquals.ts b/src/@lib/equalities/shallowEquals.ts index 56bf6660..da7087e9 100644 --- a/src/@lib/equalities/shallowEquals.ts +++ b/src/@lib/equalities/shallowEquals.ts @@ -1,3 +1,37 @@ export function shallowEquals(objA: T, objB: T): boolean { - return objA === objB; + // 두 객체의 참조 레퍼런스 값이 같은 경우, 또는 기본 타입인데 같은 경우 + if (objA === objB) { + return true; + } + + // 두 객체 중 하나라도 객체가 아니거나 null인 경우 + if ( + typeof objA !== "object" || + objA === null || + typeof objB !== "object" || + objB === null + ) { + 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; + } + + // 모든 키에 대해서 얕은 비교 수행 + // general하게 사용되는 shallowEquals이므로, Object.prototype.hasOwnProperty를 사용 (call로 매핑해줌으로써, 예외상황을 처리) + for (const key of keysA) { + if ( + !Object.prototype.hasOwnProperty.call(objB, key) || + objA[key] !== objB[key] + ) { + return false; + } + } + + return true; } diff --git a/src/@lib/hocs/memo.ts b/src/@lib/hocs/memo.ts index d43559d0..03b84523 100644 --- a/src/@lib/hocs/memo.ts +++ b/src/@lib/hocs/memo.ts @@ -1,10 +1,22 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { shallowEquals } from "../equalities"; -import { ComponentType } from "react"; +import React, { ComponentType, ReactNode } from "react"; export function memo

( Component: ComponentType

, _equals = shallowEquals, ) { - return Component; + // ref에 대한 의문, hooks가 아닌데, ref를 사용하는게 맞는가? -> hooks라는 전제를 깔자. + // TODO : 이거 관련해서 다시 한번 깊게 탐구해보기. Ref가 진짜로 필요한가? 그리고, 만약 클로져로 처리했을 때 리랜더링이 안일어나나? + // let preProps = useRef

(null); + let preProps: P | null = null; + let memoizedComponent: ReactNode | null = null; + + return function (props: P) { + if (preProps === null || !_equals(preProps, props)) { + preProps = props; + // jsx 였으면, 컴포넌트 생성해도 좋았겠지만, 결국 이것도 오버헤드일 것 같기에 createElement로 진행 + memoizedComponent = React.createElement(Component, props); + } + return memoizedComponent; + }; } diff --git a/src/@lib/hooks/useCallback.ts b/src/@lib/hooks/useCallback.ts index e71e647d..cafdac39 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 */ +/* eslint-disable @typescript-eslint/no-unsafe-function-type */ import { DependencyList } from "react"; +import { useMemo } from "./useMemo"; export function useCallback( factory: T, _deps: DependencyList, ) { - // 직접 작성한 useMemo를 통해서 만들어보세요. - return factory as T; + const memoizedFactory = useMemo(() => factory, _deps); + return memoizedFactory as T; } diff --git a/src/@lib/hooks/useMemo.ts b/src/@lib/hooks/useMemo.ts index 95930d69..bc2b3d38 100644 --- a/src/@lib/hooks/useMemo.ts +++ b/src/@lib/hooks/useMemo.ts @@ -1,12 +1,23 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { DependencyList } from "react"; import { shallowEquals } from "../equalities"; +import { useRef } from "./useRef"; export function useMemo( factory: () => T, _deps: DependencyList, _equals = shallowEquals, ): T { - // 직접 작성한 useRef를 통해서 만들어보세요. - return factory(); + const prevDeps = useRef(null); + const prevResult = useRef(null); + + if ( + prevResult.current === null || + prevDeps.current === null || + !_equals(prevDeps.current, _deps) + ) { + prevDeps.current = _deps; + prevResult.current = factory(); + } + + return prevResult.current; } diff --git a/src/@lib/hooks/useRef.ts b/src/@lib/hooks/useRef.ts index 2dc9e83f..a0e159be 100644 --- a/src/@lib/hooks/useRef.ts +++ b/src/@lib/hooks/useRef.ts @@ -1,4 +1,8 @@ +import { useState } from "react"; + export function useRef(initialValue: T): { current: T } { - // React의 useState를 이용해서 만들어보세요. - return { current: initialValue }; + // useRef, useState 모두 컴포넌트의 라이프사이클 동안 값을 유지하는 게 필요함. + // useState는 값의 변동이 있을 때 리랜더링이 일어남. 참조의 경우도 참조값이 변해야 함. 그래서, useState의 기본 기능을 활용하되, 참조값을 업데이트 하지 않는 방식으로 구현. + const [refObject] = useState({ current: initialValue }); + return refObject; } diff --git a/src/App.tsx b/src/App.tsx index debd6454..22c06e58 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,335 +1,49 @@ -import React, { useState, createContext, useContext } from "react"; -import { generateItems, renderLog } from "./utils"; +import React, { useState, useCallback } from "react"; +import { + Header, + ItemList, + ComplexForm, + NotificationSystem, +} from "./components"; +import { Item } from "./types"; +import { generateItems } from "./utils"; +import { ThemeProvider, UserProvider, NotificationProvider } from "./contexts"; -// 타입 정의 -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} - -
- ))} -
- ); -}; - -// 메인 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 [items, setItems] = useState(generateItems(1000)); - const toggleTheme = () => { - setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light")); - }; - - const addItems = () => { + const addItems = useCallback(() => { setItems((prevItems) => [ ...prevItems, ...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 = { - id: Date.now(), - message, - type, - }; - setNotifications((prev) => [...prev, newNotification]); - }; + }, []); - const removeNotification = (id: number) => { - setNotifications((prev) => - prev.filter((notification) => notification.id !== id), - ); - }; + return ( + + + + + + + + ); +}; - const contextValue: AppContextType = { - theme, - toggleTheme, - user, - login, - logout, - notifications, - addNotification, - removeNotification, - }; +interface MainAppProps { + items: Item[]; + onAddItemsClick: () => void; +} - return ( - -
+const MainApp: React.FC = React.memo( + ({ items, onAddItemsClick }) => { + return ( +
- +
@@ -338,8 +52,8 @@ const App: React.FC = () => {
- - ); -}; + ); + }, +); export default App; diff --git a/src/components/ComplexForm.tsx b/src/components/ComplexForm.tsx new file mode 100644 index 00000000..a12760ad --- /dev/null +++ b/src/components/ComplexForm.tsx @@ -0,0 +1,116 @@ +import { useState } from "react"; +import { useCallback, useMemo } from "../@lib/hooks"; +import { memo } from "../@lib/hocs"; +import { renderLog } from "../utils"; +import { useNotificationActions, useNotificationState } from "../hooks"; + +export const ComplexForm: React.FC = memo(() => { + renderLog("ComplexForm rendered"); + const { addNotification } = useNotificationActions(); + + // notifications 상태를 구독 + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { notifications } = useNotificationState(); + + const [formData, setFormData] = useState({ + name: "", + email: "", + age: 0, + preferences: [] as string[], + }); + + const handleSubmit = useCallback( + (e: React.FormEvent) => { + e.preventDefault(); + // 폼 유효성 검사 + if (!formData.name || !formData.email) { + addNotification("이름과 이메일은 필수 입력 사항입니다.", "warning"); + return; + } + addNotification("폼이 성공적으로 제출되었습니다", "success"); + }, + [addNotification, formData.name, formData.email], + ); + + const handleInputChange = useCallback( + (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ + ...prev, + [name]: name === "age" ? parseInt(value) || 0 : value, + })); + }, + [], + ); + + const handlePreferenceChange = useCallback((preference: string) => { + setFormData((prev) => ({ + ...prev, + preferences: prev.preferences.includes(preference) + ? prev.preferences.filter((p) => p !== preference) + : [...prev.preferences, preference], + })); + }, []); + + // useMemo를 사용하여 폼 유효성 메시지를 메모이제이션 + const validationMessage = useMemo(() => { + if (!formData.name || !formData.email) { + return "이름과 이메일을 모두 입력해주세요."; + } + return null; + }, [formData.name, formData.email]); + + return ( +
+

복잡한 폼

+
+ + + +
+ {["독서", "운동", "음악", "여행"].map((pref) => ( + + ))} +
+ {validationMessage && ( +
{validationMessage}
+ )} + +
+
+ ); +}); diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 00000000..3f210895 --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,63 @@ +import { memo } from "../@lib/hocs"; +import { useCallback, useMemo } from "../@lib/hooks"; +import { renderLog } from "../utils"; +import { useTheme, useUser } from "../hooks"; +import { useNotificationActions } from "../hooks/useNotificationActions"; + +export const Header: React.FC = memo(() => { + renderLog("Header rendered"); + const { theme, toggleTheme } = useTheme(); + const { user, login, logout } = useUser(); + const { addNotification } = useNotificationActions(); + + const handleLogin = useCallback(() => { + login("user@example.com", "123"); + addNotification("성공적으로 로그인되었습니다", "success"); + }, [login, addNotification]); + + const handleLogout = useCallback(() => { + logout(); + addNotification("로그아웃되었습니다", "info"); + }, [logout, addNotification]); + + const themeButtonText = useMemo(() => { + return theme === "light" ? "다크 모드" : "라이트 모드"; + }, [theme]); + + const buttonClass = useMemo(() => { + return theme === "light" + ? "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mr-2" + : "bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mr-2"; + }, [theme]); + + return ( +
+
+

샘플 애플리케이션

+
+ + {user ? ( +
+ {user.name}님 환영합니다! + +
+ ) : ( + + )} +
+
+
+ ); +}); diff --git a/src/components/ItemList.tsx b/src/components/ItemList.tsx new file mode 100644 index 00000000..8c7a70b7 --- /dev/null +++ b/src/components/ItemList.tsx @@ -0,0 +1,75 @@ +import { useState } from "react"; +import { memo } from "../@lib/hocs"; +import { useMemo } from "../@lib/hooks"; +import { renderLog } from "../utils"; +import { useTheme } from "../hooks"; +import { Item } from "../types"; + +// ItemList 컴포넌트 +export const ItemList: React.FC<{ + items: Item[]; + onAddItemsClick: () => void; +}> = memo(({ items, onAddItemsClick }) => { + renderLog("ItemList rendered"); + const [filter, setFilter] = useState(""); + const { theme } = useTheme(); + + const filteredItems = useMemo( + () => + items.filter( + (item) => + item.name.toLowerCase().includes(filter.toLowerCase()) || + item.category.toLowerCase().includes(filter.toLowerCase()), + ), + [items, filter], + ); + + const totalPrice = useMemo( + () => filteredItems.reduce((sum, item) => sum + item.price, 0), + [filteredItems], + ); + + const averagePrice = useMemo( + () => Math.round(totalPrice / filteredItems.length) || 0, + [totalPrice, filteredItems.length], + ); + + 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/NotificationItem.tsx b/src/components/NotificationItem.tsx new file mode 100644 index 00000000..17a31045 --- /dev/null +++ b/src/components/NotificationItem.tsx @@ -0,0 +1,29 @@ +import { memo } from "../@lib/hocs/memo"; +import { Notification } from "../types"; + +export const NotificationItem: React.FC<{ + notification: Notification; + onRemove: (id: number) => void; +}> = memo(({ notification, onRemove }) => { + return ( +
+ {notification.message} + +
+ ); +}); diff --git a/src/components/NotificationSystem.tsx b/src/components/NotificationSystem.tsx new file mode 100644 index 00000000..beaeae82 --- /dev/null +++ b/src/components/NotificationSystem.tsx @@ -0,0 +1,36 @@ +import { memo } from "../@lib/hocs/memo"; +import { useCallback, useMemo } from "../@lib/hooks"; +import { renderLog } from "../utils"; +import { useNotificationState } from "../hooks/useNotificationState"; +import { useNotificationActions } from "../hooks/useNotificationActions"; +import { NotificationItem } from "./NotificationItem"; + +export const NotificationSystem: React.FC = memo(() => { + renderLog("NotificationSystem rendered"); + const { notifications } = useNotificationState(); + const { removeNotification } = useNotificationActions(); + + const handleRemove = useCallback( + (id: number) => { + removeNotification(id); + }, + [removeNotification], + ); + + // useMemo를 사용하여 알림 목록을 메모이제이션 + const renderedNotifications = useMemo(() => { + return notifications.map((notification) => ( + + )); + }, [notifications, handleRemove]); + + return ( +
+ {renderedNotifications} +
+ ); +}); diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 00000000..d7e27181 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,5 @@ +export * from "./ComplexForm"; +export * from "./Header"; +export * from "./ItemList"; +export * from "./NotificationSystem"; +export * from "./NotificationItem"; diff --git a/src/contexts/NotificationContext.tsx b/src/contexts/NotificationContext.tsx new file mode 100644 index 00000000..4cc56f22 --- /dev/null +++ b/src/contexts/NotificationContext.tsx @@ -0,0 +1,61 @@ +// contexts/NotificationContext.tsx + +import React, { + createContext, + useCallback, + useState, + ReactNode, + useMemo, +} from "react"; +import { Notification } from "../types"; + +interface NotificationStateContextProps { + notifications: Notification[]; +} + +interface NotificationActionsContextProps { + addNotification: (message: string, type: string) => void; + removeNotification: (id: number) => void; +} + +export const NotificationStateContext = createContext< + NotificationStateContextProps | undefined +>(undefined); +export const NotificationActionsContext = createContext< + NotificationActionsContextProps | undefined +>(undefined); + +export const NotificationProvider: React.FC<{ children: ReactNode }> = ({ + children, +}) => { + const [notifications, setNotifications] = useState([]); + + const addNotification = useCallback((message: string, type: string) => { + const newNotification: Notification = { + id: Date.now(), + message, + type, + }; + setNotifications((prev) => [...prev, newNotification]); + }, []); + + const removeNotification = useCallback((id: number) => { + setNotifications((prev) => + prev.filter((notification) => notification.id !== id), + ); + }, []); + + // actions 객체를 useMemo로 메모이제이션 + const actions = useMemo( + () => ({ addNotification, removeNotification }), + [addNotification, removeNotification], + ); + + return ( + + + {children} + + + ); +}; diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx new file mode 100644 index 00000000..b99a4051 --- /dev/null +++ b/src/contexts/ThemeContext.tsx @@ -0,0 +1,22 @@ +import React, { createContext, useState, useCallback, ReactNode } from "react"; +import type { ThemeContextType } from "../types"; + +export const ThemeContext = createContext( + undefined, +); + +export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ + children, +}) => { + const [theme, setTheme] = useState("light"); + + const toggleTheme = useCallback(() => { + setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light")); + }, []); + + return ( + + {children} + + ); +}; diff --git a/src/contexts/UserContext.tsx b/src/contexts/UserContext.tsx new file mode 100644 index 00000000..0d76a841 --- /dev/null +++ b/src/contexts/UserContext.tsx @@ -0,0 +1,28 @@ +import React, { createContext, useState, useCallback, ReactNode } from "react"; +import { User } from "../types"; +import type { UserContextType } from "../types"; + +export const UserContext = createContext( + undefined, +); + +export const UserProvider: React.FC<{ children: ReactNode }> = ({ + children, +}) => { + const [user, setUser] = useState(null); + + const login = useCallback((email: string, password: string) => { + setUser({ id: 1, name: "홍길동", email }); + console.log(password); + }, []); + + const logout = useCallback(() => { + setUser(null); + }, []); + + return ( + + {children} + + ); +}; diff --git a/src/contexts/index.ts b/src/contexts/index.ts new file mode 100644 index 00000000..8e756856 --- /dev/null +++ b/src/contexts/index.ts @@ -0,0 +1,3 @@ +export * from "./ThemeContext"; +export * from "./UserContext"; +export * from "./NotificationContext"; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 00000000..8cb6e972 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,4 @@ +export * from "./useTheme"; +export * from "./useUser"; +export * from "./useNotificationActions"; +export * from "./useNotificationState"; diff --git a/src/hooks/useNotificationActions.tsx b/src/hooks/useNotificationActions.tsx new file mode 100644 index 00000000..f3647a2d --- /dev/null +++ b/src/hooks/useNotificationActions.tsx @@ -0,0 +1,13 @@ +import { useContext } from "react"; +import { NotificationActionsContext } from "../contexts/NotificationContext"; +import type { NotificationActionsContextProps } from "../types"; + +export const useNotificationActions = (): NotificationActionsContextProps => { + const context = useContext(NotificationActionsContext); + if (context === undefined) { + throw new Error( + "useNotificationActions must be used within a NotificationProvider", + ); + } + return context; +}; diff --git a/src/hooks/useNotificationState.tsx b/src/hooks/useNotificationState.tsx new file mode 100644 index 00000000..32117081 --- /dev/null +++ b/src/hooks/useNotificationState.tsx @@ -0,0 +1,13 @@ +import { useContext } from "react"; +import { NotificationStateContext } from "../contexts/NotificationContext"; +import type { NotificationStateContextProps } from "../types"; + +export const useNotificationState = (): NotificationStateContextProps => { + const context = useContext(NotificationStateContext); + if (context === undefined) { + throw new Error( + "useNotificationState must be used within a NotificationProvider", + ); + } + return context; +}; diff --git a/src/hooks/useTheme.tsx b/src/hooks/useTheme.tsx new file mode 100644 index 00000000..f3618f7c --- /dev/null +++ b/src/hooks/useTheme.tsx @@ -0,0 +1,11 @@ +import { useContext } from "react"; +import { ThemeContext } from "../contexts"; +import type { ThemeContextType } from "../types"; + +export const useTheme = (): ThemeContextType => { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + return context; +}; diff --git a/src/hooks/useUser.tsx b/src/hooks/useUser.tsx new file mode 100644 index 00000000..81a3e8d6 --- /dev/null +++ b/src/hooks/useUser.tsx @@ -0,0 +1,11 @@ +import { useContext } from "react"; +import { UserContext } from "../contexts"; +import type { UserContextType } from "../types"; + +export const useUser = (): UserContextType => { + const context = useContext(UserContext); + if (context === undefined) { + throw new Error("useUser must be used within a UserProvider"); + } + return context; +}; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 00000000..8183c662 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,4 @@ +export * from "./types"; +export * from "./themeContextTypes"; +export * from "./userContextTypes"; +export * from "./notificationContextTypes"; diff --git a/src/types/notificationContextTypes.ts b/src/types/notificationContextTypes.ts new file mode 100644 index 00000000..ce02d656 --- /dev/null +++ b/src/types/notificationContextTypes.ts @@ -0,0 +1,16 @@ +import { Notification, NotificationType } from "./"; + +export interface NotificationContextType { + notifications: Notification[]; + addNotification: (message: string, type: NotificationType) => void; + removeNotification: (id: number) => void; +} + +export interface NotificationStateContextProps { + notifications: Notification[]; +} + +export interface NotificationActionsContextProps { + addNotification: (message: string, type: string) => void; + removeNotification: (id: number) => void; +} diff --git a/src/types/themeContextTypes.ts b/src/types/themeContextTypes.ts new file mode 100644 index 00000000..bc34f4b4 --- /dev/null +++ b/src/types/themeContextTypes.ts @@ -0,0 +1,4 @@ +export interface ThemeContextType { + theme: string; + toggleTheme: () => void; +} diff --git a/src/types/types.ts b/src/types/types.ts new file mode 100644 index 00000000..ed0decb2 --- /dev/null +++ b/src/types/types.ts @@ -0,0 +1,33 @@ +// 타입 정의 +export interface Item { + id: number; + name: string; + category: string; + price: number; +} + +export interface User { + id: number; + name: string; + email: string; +} + +export type NotificationType = "info" | "success" | "warning" | "error"; + +export interface Notification { + id: number; + message: string; + type: NotificationType; +} + +// 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; +} diff --git a/src/types/userContextTypes.ts b/src/types/userContextTypes.ts new file mode 100644 index 00000000..cff8866c --- /dev/null +++ b/src/types/userContextTypes.ts @@ -0,0 +1,7 @@ +import { User } from "./"; + +export interface UserContextType { + user: User | null; + login: (email: string, password: string) => void; + logout: () => void; +}