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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
}
};
</script>

</head>
<body class="bg-gray-50">
<div id="root"></div>
Expand Down
71 changes: 48 additions & 23 deletions packages/app/src/components/toast/ToastProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,77 @@
/* eslint-disable react-refresh/only-export-components */
import { createContext, memo, type PropsWithChildren, useContext, useReducer } from "react";
import { createPortal } from "react-dom";
import { Toast } from "./Toast";
import { createActions, initialState, toastReducer, type ToastType } from "./toastReducer";
import { initialState, toastReducer, type ToastType, useToastActions } from "./toastReducer";
import { debounce } from "../../utils";
import { useAutoCallback } from "@hanghae-plus/lib";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상수님도 useAutoCallback으로 관리를 해주셨군요
유열님이 구성해주신 React 내장 객체 useCallBack도 한번 확인하면 좋을 것 같아요!

https://github.com/hanghae-plus/front_6th_chapter1-3/pull/15/files

import { useMemo } from "@hanghae-plus/lib/src/hooks";

type ShowToast = (message: string, type: ToastType) => void;
type Hide = () => void;

const ToastContext = createContext<{
message: string;
type: ToastType;
// toast command를 관리하는 context
const ToastCommandContext = createContext<{
show: ShowToast;
hide: Hide;
}>({
...initialState,
show: () => null,
hide: () => null,
});

// toast state를 관리하는 context
const ToastStateContext = createContext<{
message: string;
type: ToastType;
}>({
...initialState,
});

const DEFAULT_DELAY = 3000;

const useToastContext = () => useContext(ToastContext);
export const useToastCommand = () => {
const { show, hide } = useToastContext();
return { show, hide };
};
export const useToastState = () => {
const { message, type } = useToastContext();
return { message, type };
};
// context를 사용하는 함수
export const useToastCommand = () => useContext(ToastCommandContext);
export const useToastState = () => useContext(ToastStateContext);

export const ToastProvider = memo(({ children }: PropsWithChildren) => {
const [state, dispatch] = useReducer(toastReducer, initialState);
const { show, hide } = createActions(dispatch);
const visible = state.message !== "";

const hideAfter = debounce(hide, DEFAULT_DELAY);
// action을 생성
const { show, hide } = useToastActions(dispatch);

const showWithHide: ShowToast = (...args) => {
// hide 함수를 3초 후에 호출
const hideAfter = useMemo(() => debounce(hide, DEFAULT_DELAY), [hide]);

// show 함수에 hideAfter 함수를 추가
const showWithHide: ShowToast = useAutoCallback((...args) => {
show(...args);
hideAfter();
};
});

// Command context value를 메모이제이션
const commandValue = useMemo(
() => ({
show: showWithHide,
hide,
}),
[showWithHide, hide],
);

// State context value를 메모이제이션
const stateValue = useMemo(
() => ({
message: state.message,
type: state.type,
}),
[state.message, state.type],
);

return (
<ToastContext value={{ show: showWithHide, hide, ...state }}>
{children}
{visible && createPortal(<Toast />, document.body)}
</ToastContext>
<ToastCommandContext.Provider value={commandValue}>
<ToastStateContext.Provider value={stateValue}>
{children}
{visible && createPortal(<Toast />, document.body)}
</ToastStateContext.Provider>
</ToastCommandContext.Provider>
);
});
17 changes: 12 additions & 5 deletions packages/app/src/components/toast/toastReducer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ActionDispatch } from "react";
import { type ActionDispatch } from "react";
import { useCallback, useMemo } from "@hanghae-plus/lib/src/hooks";

export type ToastType = "info" | "success" | "warning" | "error";

Expand Down Expand Up @@ -33,7 +34,13 @@ export const toastReducer = (state: ToastState, action: any): ToastState => {
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const createActions = (dispatch: ActionDispatch<[action: any]>) => ({
show: (message: string, type: ToastType) => dispatch({ type: Actions.SHOW, payload: { message, type } }),
hide: () => dispatch({ type: Actions.HIDE }),
});
export const useToastActions = (dispatch: ActionDispatch<[action: any]>) => {
const show = useCallback(
(message: string, type: ToastType) => dispatch({ type: Actions.SHOW, payload: { message, type } }),
[dispatch],
);

const hide = useCallback(() => dispatch({ type: Actions.HIDE }), [dispatch]);

return useMemo(() => ({ show, hide }), [show, hide]);
};
2 changes: 1 addition & 1 deletion packages/lib/src/createObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ type Listener = () => void;
export const createObserver = () => {
const listeners = new Set<Listener>();

// useSyncExternalStore 에서 활용할 수 있도록 subscribe 함수를 수정합니다.
const subscribe = (fn: Listener) => {
listeners.add(fn);
return () => unsubscribe(fn);
};

const unsubscribe = (fn: Listener) => {
Expand Down
29 changes: 28 additions & 1 deletion packages/lib/src/equals/deepEquals.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
import { typeUtils } from "../utils/typeUtil.ts";
import { deepEqualsArrays } from "./deepEqualsArrays.ts";
import deepEqualsObjects from "./deepEqualsObjects.ts";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 함수 util로 빼주셧네요 깔끔해요


export const deepEquals = (a: unknown, b: unknown) => {
return a === b;
// 원시 타입인 경우
if (typeUtils.isPrimitive(a) || typeUtils.isPrimitive(b)) {
return a === b;
}

const aIsArray = Array.isArray(a);
const bIsArray = Array.isArray(b);

// 비교하려는 값의 타입이 다르면 false
if (aIsArray !== bIsArray) {
return false;
}

// 배열인 경우
if (aIsArray && bIsArray) {
return deepEqualsArrays(a, b);
}

// 객체인 경우
if (typeUtils.isObject(a) && typeUtils.isObject(b)) {
return deepEqualsObjects(a, b);
}

return true;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

??마지막에 모든 조건 다 지나가고 무조건 true 값을 던져주네요??
명시적으로 안맞지 않나 라는 생각이들어요
return a === b;가 맞다는 생각이 드네요

};
32 changes: 32 additions & 0 deletions packages/lib/src/equals/deepEqualsArrays.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { deepEquals } from "./deepEquals.ts";

type ArrayValue = unknown[];

export const deepEqualsArrays = (arrayA: ArrayValue, arrayB: ArrayValue) => {
// 참조가 같으면 true
if (arrayA === arrayB) {
return true;
}

// 둘 중 하나라도 null 또는 undefined면 false
if (!arrayA || !arrayB) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분도 아래 배열을 인자값으로 받는다고 타입명시를 했는데 다시한번 선언해주셧네요

return false;
}

const arrayALength = arrayA.length;
const arrayBLength = arrayB.length;

// 길이가 다르면 false
if (arrayALength !== arrayBLength) {
return false;
}

for (let i = 0; i < arrayALength; i++) {
// 각 요소를 재귀적으로 비교
if (!deepEquals(arrayA[i], arrayB[i])) {
return false;
}
}

return true;
};
41 changes: 41 additions & 0 deletions packages/lib/src/equals/deepEqualsObjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { deepEquals } from "./deepEquals.ts";

type ObjectValue = Record<string, unknown>;

export default function deepEqualsObjects(objectA: ObjectValue, objectB: ObjectValue): boolean {
// 참조가 같으면 true
if (objectA === objectB) {
return true;
}

// 둘 중 하나라도 null 또는 undefined면 false
if (!objectA || !objectB) {
return false;
}

const objectAKeys = Object.keys(objectA);
const objectBKeys = Object.keys(objectB);
const objectALength = objectAKeys.length;
const objectBLength = objectBKeys.length;

// 키가 개수가 다르면 false
if (objectALength !== objectBLength) {
return false;
}

for (let i = 0; i < objectALength; i++) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반복문이 deepEquals를 호출을 안하네요?

  if (!objectB.hasOwnProperty(key)) {
    return false;
  }

  if (!deepEquals(objectA[key], objectB[key])) {
    return false;
  }

이게 빠진것같은데...맞을까요??
아니면 아예 직관적으로 for...in 루프보다 Object.keys() + 루프가 더 안전하고 직관적이지 않을까 생각이드네요

const key = objectAKeys[i];

// B에 해당 key가 없으면 다름
if (!Object.hasOwn(objectB, key)) {
return false;
}

// 각 요소를 재귀적으로 비교
if (!deepEquals(objectA[key], objectB[key])) {
return false;
}
}

return true;
}
27 changes: 26 additions & 1 deletion packages/lib/src/equals/shallowEquals.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
import shallowEqualsArrays from "./shallowEqualsArrays.ts";
import shallowEqualsObjects from "./shallowEqualsObjects.ts";
import { typeUtils } from "../utils/typeUtil.ts";

export const shallowEquals = (a: unknown, b: unknown) => {
return a === b;
// 원시 타입인 경우
if (typeUtils.isPrimitive(a) || typeUtils.isPrimitive(b)) {
return a === b;
}

const aIsArray = Array.isArray(a);
const bIsArray = Array.isArray(b);

// 비교하려는 값의 타입이 다르면 false
if (aIsArray !== bIsArray) {
return false;
}

// 배열인 경우
if (aIsArray && bIsArray) {
return shallowEqualsArrays(a, b);
}

// 객체인 경우
if (typeUtils.isObject(a) && typeUtils.isObject(b)) {
return shallowEqualsObjects(a, b);
}
};
30 changes: 30 additions & 0 deletions packages/lib/src/equals/shallowEqualsArrays.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
type ArrayValue = unknown[];

export default function shallowEqualsArrays(arrayA: ArrayValue, arrayB: ArrayValue) {
// 참조가 같으면 true
if (arrayA === arrayB) {
return true;
}

// 둘 중 하나라도 null 또는 undefined면 false
if (!arrayA || !arrayB) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인자값 받을떄 배열로 받고 null값으로 받을수가 있나요??
이런부분들이 타입스크립트를 사용할때 장점인 것 같아요

return false;
}

const arrayALength = arrayA.length;
const arrayBLength = arrayB.length;

// 길이가 다르면 false
if (arrayALength !== arrayBLength) {
return false;
}

for (let i = 0; i < arrayALength; i++) {
// 값이 다르면 false
if (arrayA[i] !== arrayB[i]) {
return false;
}
}

return true;
}
34 changes: 34 additions & 0 deletions packages/lib/src/equals/shallowEqualsObjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
type ObjectValue = Record<string, unknown>;

export default function shallowEqualsObjects(objectA: ObjectValue, objectB: ObjectValue): boolean {
// 참조가 같으면 true
if (objectA === objectB) {
return true;
}

// 둘 중 하나라도 null 또는 undefined면 false
if (!objectA || !objectB) {
return false;
}

const objectAKeys = Object.keys(objectA);
const objectBKeys = Object.keys(objectB);
const objectALength = objectAKeys.length;
const objectBLength = objectBKeys.length;

// 키가 개수가 다르면 false
if (objectALength !== objectBLength) {
return false;
}

for (let i = 0; i < objectALength; i++) {
const key = objectAKeys[i];

// 값이 다르거나, B에 해당 key가 없으면 다름
if (objectA[key] !== objectB[key] || !Object.hasOwn(objectB, key)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분 생각보다 순서가 중요해서 key가 없는데 접근해서 undefined !== undefined로 통과될 수 있어요

if (!Object.hasOwn(objectB, key) || objectA[key] !== objectB[key]) {
  return false;
}

이렇게 구성하기를 권장드려요

return false;
}
}

return true;
}
4 changes: 3 additions & 1 deletion packages/lib/src/hocs/deepMemo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { FunctionComponent } from "react";
import { memo } from "./memo.ts";
import { deepEquals } from "../equals";

export function deepMemo<P extends object>(Component: FunctionComponent<P>) {
return Component;
return memo(Component, deepEquals);
}
2 changes: 1 addition & 1 deletion packages/lib/src/hocs/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from "./deepMemo";
export * from "./memo";
export * from "./memo.ts";
25 changes: 23 additions & 2 deletions packages/lib/src/hocs/memo.ts

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍 주석이 잘 달려 잇어서 확인이 편하네요

Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
import { type FunctionComponent } from "react";
import type { FunctionComponent, ReactNode } from "react";
import { shallowEquals } from "../equals";
import { useRef } from "../hooks";

export function memo<P extends object>(Component: FunctionComponent<P>, equals = shallowEquals) {
return Component;
return function MemoizedComponent(props: P) {
const prevPropsRef = useRef<P | null>(null);
const prevComponentRef = useRef<ReactNode | Promise<ReactNode> | null>(null);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promise 로 들어가야되는 이유가 혹시 잇을까요???
헷갈리네요


// 첫 렌더링이면, 이전 컴포넌트를 생성하고 반환한다.
if (!prevPropsRef.current) {
prevPropsRef.current = props;
prevComponentRef.current = Component({ ...props });
return prevComponentRef.current;
}

// props가 변경되지 않았다면, 이전 컴포넌트를 반환한다.
if (equals(prevPropsRef.current, props)) {
return prevComponentRef.current;
}

// props가 변경되었으므로, 이전 컴포넌트를 삭제하고 새로운 컴포넌트를 생성한다.
prevPropsRef.current = props;
prevComponentRef.current = Component({ ...props });
return prevComponentRef.current;
};
}
Loading