Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[6팀 방재현] [Chater 1-3] React, Beyond the Basics #60

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"trailingComma": "none"
}
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"tsc --noEmit",
"tsc --noEmit --jsx react-jsx",
"prettier --write",
"eslint --fix"
]
Expand All @@ -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",
Expand Down
47 changes: 47 additions & 0 deletions src/@context/notiContext.tsx
Original file line number Diff line number Diff line change
@@ -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<INotificationContext | undefined>(
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 }: { children: ReactNode }) => {
const [notifications, setNotifications] = useState<INotification[]>([]);

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 (
<NotiContext.Provider value={contextValue}>{children}</NotiContext.Provider>
);
};
40 changes: 40 additions & 0 deletions src/@context/themeContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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<IThemeContext | undefined>(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 (
<ThemeContext.Provider value={contextValue}>
<div
className={`min-h-screen ${theme === "light" ? "bg-gray-100" : "bg-gray-900 text-white"}`}
>
{children}
</div>
</ThemeContext.Provider>
);
};
41 changes: 41 additions & 0 deletions src/@context/userContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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<IUserContext | undefined>(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<IUser | null>(null);
const { addNotification } = useNotiContext();

const login = useCallback((email: string) => {
setUser({ id: 1, name: "홍길동", email });
addNotification("성공적으로 로그인되었습니다", "success");
}, []);
// const logout = useCallback(() => {}, []);
const logout = useCallback(() => {
setUser(null);
addNotification("로그아웃 되었습니다.", "success");
}, [addNotification]);

const contextValue = useMemo(
() => ({
user,
login,
logout
}),
[user, login, logout]
);
return (
<UserContext.Provider value={contextValue}>{children}</UserContext.Provider>
);
};
33 changes: 32 additions & 1 deletion src/@lib/equalities/deepEquals.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
export function deepEquals<T>(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;
}
20 changes: 19 additions & 1 deletion src/@lib/equalities/shallowEquals.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
export function shallowEquals<T>(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;
}
13 changes: 11 additions & 2 deletions src/@lib/hocs/memo.ts
Original file line number Diff line number Diff line change
@@ -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<P extends object>(
Component: ComponentType<P>,
_equals = shallowEquals,
_equals = shallowEquals
) {
return Component;
const memoComponent = (props: P) => {
const prevElement = useRef<P | null>(null);
if (prevElement.current === null || !_equals(prevElement.current, props)) {
prevElement.current = props;
return React.createElement(Component, prevElement.current);
}
};
return memoComponent;
}
7 changes: 4 additions & 3 deletions src/@lib/hooks/useCallback.ts
Original file line number Diff line number Diff line change
@@ -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<T extends Function>(
factory: T,
_deps: DependencyList,
) {
// 직접 작성한 useMemo를 통해서 만들어보세요.
return factory as T;
const callbackMemo = useMemo(() => factory, _deps);
return callbackMemo;
}
12 changes: 10 additions & 2 deletions src/@lib/hooks/useMemo.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { DependencyList } from "react";
import { shallowEquals } from "../equalities";
import { useRef } from "./useRef";

export function useMemo<T>(
factory: () => T,
_deps: DependencyList,
_equals = shallowEquals,
): T {
// 직접 작성한 useRef를 통해서 만들어보세요.
return factory();
const value = useRef<T | null>(null);
const deps = useRef(_deps);

if (value.current === null || !_equals(deps.current, _deps)) {
value.current = factory();
deps.current = _deps;
}

return value.current;
}
5 changes: 4 additions & 1 deletion src/@lib/hooks/useRef.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { useState } from "react";

export function useRef<T>(initialValue: T): { current: T } {
// React의 useState를 이용해서 만들어보세요.
return { current: initialValue };
const [ref] = useState({ current: initialValue });
return ref;
}
Loading
Loading