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
38 changes: 21 additions & 17 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### 배포 링크

https://hwirin-kim.github.io/front_6th_chapter1-3/

<!--
배포 링크를 적어주세요
예시: https://<username>.github.io/front-6th-chapter1-3/
Expand All @@ -14,41 +16,43 @@

#### equalities

- [ ] shallowEquals 구현 완료
- [ ] deepEquals 구현 완료
- [x] shallowEquals 구현 완료
- [x] deepEquals 구현 완료

#### hooks

- [ ] useRef 구현 완료
- [ ] useMemo 구현 완료
- [ ] useCallback 구현 완료
- [ ] useDeepMemo 구현 완료
- [ ] useShallowState 구현 완료
- [ ] useAutoCallback 구현 완료
- [x] useRef 구현 완료
- [x] useMemo 구현 완료
- [x] useCallback 구현 완료
- [x] useDeepMemo 구현 완료
- [x] useShallowState 구현 완료
- [x] useAutoCallback 구현 완료

#### High Order Components

- [ ] memo 구현 완료
- [ ] deepMemo 구현 완료
- [x] memo 구현 완료
- [x] deepMemo 구현 완료

### 심화 과제

#### hooks

- [ ] createObserver를 useSyncExternalStore에 사용하기 적합한 코드로 개선
- [ ] useShallowSelector 구현
- [ ] useStore 구현
- [ ] useRouter 구현
- [ ] useStorage 구현
- [x] createObserver를 useSyncExternalStore에 사용하기 적합한 코드로 개선
- [x] useShallowSelector 구현
- [x] useStore 구현
- [x] useRouter 구현
- [x] useStorage 구현

### context

- [ ] ToastContext, ModalContext 개선
- [x] ToastContext, ModalContext 개선

## 과제 셀프회고

<!-- 과제에 대한 회고를 작성해주세요 -->

이번 과제에서는 여러가지 리액트 훅을 만들어보았다.

### 기술적 성장

<!-- 예시
Expand Down Expand Up @@ -142,7 +146,7 @@

과제에서 디테일한 피드백을 받기 위해선 여러분의 생각을 디테일하게 표현해주셔야 한답니다.

가령, "전반적으로 이 라우터 구조가 규모가 커졌을 때 유지보수나 기능 확장에 유리한지, 아니면 리팩토링이 필요할지 조언을 받고 싶습니다" 라는 질문이 있을 때, 답변드리기가 어려워요.
가령, "전반적으로 이 라우터 구조가 규모가 커졌을 때 유지보수나 기능 확장에 유리한지, 아니면 리팩토링이 필요할지 조언을 받고 싶습니다" 라는 질문이 있을 때, 답변드리기가 어려워요.
이럴 때는 "기능 확장" 상황을 먼저 가정해봐야합니다. 테스트의 엣지케이스를 작성하는 것 처럼요! 그리고 그 상황에 대해 내가 작성한 코드가 이러저러한 이유 때문에 대응가능할 것 같은데 혹시 더 고려해야할 부분이 있을지를 물어보는거죠.

이건 코치에게 이야기할 때 뿐만 아니라 팀원에게 이야기할 때에도 동일해요. 여러분의 컨텍스트를 명확하게 전달하지 않으면 여러분과 이야기할 때 시간이 무척 오래 걸린답니다.
Expand Down
5 changes: 5 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ export default tseslint.config([
...tseslint.configs.recommended,
eslintPluginPrettier,
eslintConfigPrettier,
{
rules: {
"@typescript-eslint/no-unused-vars": "off",
},
},
]);
57 changes: 36 additions & 21 deletions packages/app/src/components/toast/ToastProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,64 @@ import { createPortal } from "react-dom";
import { Toast } from "./Toast";
import { createActions, initialState, toastReducer, type ToastType } from "./toastReducer";
import { debounce } from "../../utils";
import { useCallback, useMemo } from "@hanghae-plus/lib/src/hooks";

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

const ToastContext = createContext<{
// 상태 컨텍스트
const ToastStateContext = createContext<{
message: string;
type: ToastType;
}>({
...initialState,
});

// 액션 컨텍스트
const ToastActionContext = createContext<{
show: ShowToast;
hide: Hide;
}>({
...initialState,
show: () => null,
hide: () => null,
});

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 };
};
export const useToastCommand = () => useContext(ToastActionContext);
export const useToastState = () => useContext(ToastStateContext);

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

// 메모이제이션 처리
const { show, hide } = useMemo(() => createActions(dispatch), [dispatch]);
const visible = state.message !== "";

const hideAfter = debounce(hide, DEFAULT_DELAY);
// 메모이제이션 처리
const hideAfter = useMemo(() => debounce(hide, DEFAULT_DELAY), [hide]);

// 메모이제이션 처리
const showWithHide: ShowToast = useCallback(
(...args) => {
show(...args);
hideAfter();
},
[show, hideAfter],
);

// 상태만 메모이제이션 처리
const memoizedStateValue = useMemo(() => ({ message: state.message, type: state.type }), [state.message, state.type]);

const showWithHide: ShowToast = (...args) => {
show(...args);
hideAfter();
};
// 액션만 메모이제이션 처리
const memoizedActionValue = useMemo(() => ({ show: showWithHide, hide }), [showWithHide, hide]);

return (
<ToastContext value={{ show: showWithHide, hide, ...state }}>
{children}
{visible && createPortal(<Toast />, document.body)}
</ToastContext>
<ToastStateContext.Provider value={memoizedStateValue}>
<ToastActionContext.Provider value={memoizedActionValue}>
{children}
{visible && createPortal(<Toast />, document.body)}
</ToastActionContext.Provider>
</ToastStateContext.Provider>
);
});
1 change: 1 addition & 0 deletions packages/lib/src/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class Router<Handler extends (...args: any[]) => any> {
}

readonly subscribe = this.#observer.subscribe;
readonly unsubscribe = this.#observer.unsubscribe;

addRoute(path: string, handler: Handler) {
// 경로 패턴을 정규식으로 변환
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/src/createObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ export const createObserver = () => {

const notify = () => listeners.forEach((listener) => listener());

return { subscribe, notify };
return { subscribe, notify, unsubscribe };
};
4 changes: 2 additions & 2 deletions packages/lib/src/createStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createObserver } from "./createObserver.ts";

export const createStorage = <T>(key: string, storage = window.localStorage) => {
let data: T | null = JSON.parse(storage.getItem(key) ?? "null");
const { subscribe, notify } = createObserver();
const { subscribe, notify, unsubscribe } = createObserver();

const get = () => data;

Expand All @@ -26,5 +26,5 @@ export const createStorage = <T>(key: string, storage = window.localStorage) =>
}
};

return { get, set, reset, subscribe };
return { get, set, reset, subscribe, unsubscribe };
};
4 changes: 2 additions & 2 deletions packages/lib/src/createStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export const createStore = <S, A = (args: { type: string; payload?: unknown }) =
reducer: (state: S, action: A) => S,
initialState: S,
) => {
const { subscribe, notify } = createObserver();
const { subscribe, notify, unsubscribe } = createObserver();

let state = initialState;

Expand All @@ -18,5 +18,5 @@ export const createStore = <S, A = (args: { type: string; payload?: unknown }) =
}
};

return { getState, dispatch, subscribe };
return { getState, dispatch, subscribe, unsubscribe };
};
35 changes: 34 additions & 1 deletion packages/lib/src/equals/deepEquals.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
import { isPlainObject } from "../utils/typeChecker";

export const deepEquals = (a: unknown, b: unknown) => {
return a === b;
// 원시 데이터 타입 비교
if (Object.is(a, b)) return true;

// 배열 비교
if (Array.isArray(a) && Array.isArray(b)) {
// 길이가 다르면 false
if (a.length !== b.length) return false;
// 요소를 하나씩 비교
for (let i = 0; i < a.length; i++) {
// 요소가 다르면 false
if (!deepEquals(a[i], b[i])) return false;
}
return true;
}

// 객체 비교
if (isPlainObject(a) && isPlainObject(b)) {
// 객체 키의 개수가 다르면 false
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) return false;
// 객체 키를 하나씩 비교
for (let i = 0; i < aKeys.length; i++) {
const currentKey = aKeys[i];
// 객체 키가 다르면 false
if (!deepEquals(a[currentKey], b[currentKey])) return false;
}
return true;
}

// 객체 비교
return false;
};
35 changes: 34 additions & 1 deletion packages/lib/src/equals/shallowEquals.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
import { isPlainObject } from "../utils/typeChecker";

export const shallowEquals = (a: unknown, b: unknown) => {
return a === b;
// 원시 데이터 타입 비교
if (Object.is(a, b)) return true;

// 배열 비교
if (Array.isArray(a) && Array.isArray(b)) {
// 길이가 다르면 false
if (a.length !== b.length) return false;
// 요소를 하나씩 비교
for (let i = 0; i < a.length; i++) {
// 요소가 다르면 false
if (!Object.is(a[i], b[i])) return false;
}
return true;
}
Comment on lines +8 to +17

Choose a reason for hiding this comment

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

배열의 타입은 'object'여서 이 코드가 없어도 테스트가 통과할 거에요 지금 utils 함수에서 배열 타입 체크 부분만 빼면요!
Object.keys() 메서드를 사용시 인덱스가 key가 됩니다 :)


// 객체 비교
if (isPlainObject(a) && isPlainObject(b)) {
// 객체 키의 개수가 다르면 false
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) return false;
// 객체 키를 하나씩 비교
for (let i = 0; i < aKeys.length; i++) {
const currentKey = aKeys[i];
// 객체 키가 다르면 false
if (!Object.is(a[currentKey], b[currentKey])) return false;
}
return true;
}

// 그외 모두 false
return false;
};
17 changes: 16 additions & 1 deletion packages/lib/src/hocs/deepMemo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import type { FunctionComponent } from "react";
import { deepEquals } from "../equals";
import { useRef } from "../hooks";

export function deepMemo<P extends object>(Component: FunctionComponent<P>) {
return Component;
// 훅 사용을 위해 컴포넌트 생성
const MemoizedComponent = (props: P) => {
// 이전 프롭스와 리턴할 결과를 ref로 관리
const prevRef = useRef<{ props: P; result: React.ReactNode | null } | null>(null);

// 이전 프롭스와 현재 프롭스 비교
if (!prevRef.current || !deepEquals(prevRef.current.props, props)) {
// 프롭스가 다르면 새로 렌더링
prevRef.current = { props, result: Component(props) as React.ReactNode };
}
// 프롭스가 같으면 이전 결과 재사용
return prevRef.current.result;
};
return MemoizedComponent;

Choose a reason for hiding this comment

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

발제 자료를 보면 앞에서 만든 memo를 사용을 하라고 되어있어서 기존에 만들어두신 memo에 2번째 인자를 deepEquals 함수를 넘겨주면 더 간단하게 deepMemo를 완성할 수 있을것 같네용 :)

}
16 changes: 15 additions & 1 deletion packages/lib/src/hocs/memo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
import { type FunctionComponent } from "react";
import { shallowEquals } from "../equals";
import { useRef } from "../hooks";

export function memo<P extends object>(Component: FunctionComponent<P>, equals = shallowEquals) {
return Component;
// 훅 사용을 위해 컴포넌트 생성
const MemoizedComponent = (props: P) => {
// 이전 프롭스와 리턴할 결과를 ref로 관리
const prevRef = useRef<{ props: P; result: React.ReactNode | null } | null>(null);

// 이전 프롭스와 현재 프롭스 비교
if (!prevRef.current || !equals(prevRef.current.props, props)) {
// 프롭스가 다르면 새로 렌더링
prevRef.current = { props, result: Component(props) as React.ReactNode };
}
// 프롭스가 같으면 이전 결과 재사용
return prevRef.current.result;
};
return MemoizedComponent;
}
15 changes: 14 additions & 1 deletion packages/lib/src/hooks/useAutoCallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,18 @@ import { useCallback } from "./useCallback";
import { useRef } from "./useRef";

export const useAutoCallback = <T extends AnyFunction>(fn: T): T => {
return fn;
// 함수 자체를 값으로 관리
const ref = useRef<T>(fn);

// 함수가 바뀌었으면 최신화
if (ref.current !== fn) {
ref.current = fn;
}

// 항상 동일한 참조를 반환하면서 내부 로직만 최신화된 ref를 호출
const stableCallback = useCallback((...args: Parameters<T>): ReturnType<T> => {
return ref.current(...args);
}, []);

return stableCallback as T;
};
10 changes: 8 additions & 2 deletions packages/lib/src/hooks/useCallback.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
/* eslint-disable @typescript-eslint/no-unused-vars,@typescript-eslint/no-unsafe-function-type */
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
import type { DependencyList } from "react";
import { useMemo } from "./useMemo";

export function useCallback<T extends Function>(factory: T, _deps: DependencyList) {
// 직접 작성한 useMemo를 통해서 만들어보세요.
return factory as T;

// 함수 자체를 값이라고 생각하고 useMemo를 사용해서 구현
// eslint-disable-next-line react-hooks/exhaustive-deps
const memoizedCallback = useMemo(() => factory, _deps);

return memoizedCallback as T;
}
14 changes: 12 additions & 2 deletions packages/lib/src/hooks/useMemo.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import type { DependencyList } from "react";
import { shallowEquals } from "../equals";
import { useRef } from "./useRef";

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

// 의존성 배열과 값을 저장하는 객체를 useRef로 관리
const ref = useRef<{ deps: DependencyList; value: T } | null>(null);

// 의존성 배열이 바뀌었거나, 값이 바뀌었으면 factory 실행해서 값 저장
if (ref.current === null || !_equals(ref.current.deps, _deps)) {
const value = factory();
ref.current = { deps: _deps, value };
}

return ref.current.value;
}
Loading