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

[11팀 박근백] [Chapter 1-3] React, Beyond the Basics #6

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
faf1325
feat: shallowEquals 추가
Geunbaek Dec 29, 2024
c5d403e
feat: deepEquals 추가
Geunbaek Dec 29, 2024
02baec7
feat: useRef 추가
Geunbaek Dec 29, 2024
e8a248f
feat: useCallback 추가
Geunbaek Dec 29, 2024
75c781b
feat: useMemo & useDeepMemo 추가
Geunbaek Dec 29, 2024
4761392
feat: momo 추가
Geunbaek Dec 29, 2024
6f3cf34
feat: ThemeContext 추가
Geunbaek Dec 29, 2024
2a341a5
feat: NotificationContext 추가
Geunbaek Dec 29, 2024
fdf79bd
feat: UserContext 추가
Geunbaek Dec 29, 2024
e0d0740
feat: context App 에 적용
Geunbaek Dec 29, 2024
c17538f
refactor: 컴포넌트 분리
Geunbaek Dec 29, 2024
7bc7ebf
fix: husky tsc 스크립트 에러 수정
Geunbaek Dec 30, 2024
048e25e
refactor: 과제 첫번째 결과 app 폴더로 이동
Geunbaek Dec 30, 2024
9af873a
fix: useCallback 리액트의 useMemo 가져오던 코드 수정
Geunbaek Dec 30, 2024
a5af0f0
feat: useStore & createStore 함수 추가
Geunbaek Dec 30, 2024
d59adc0
feat: 외부 스토어를 사용한 context api 최적화 코드 app-plus 폴더에 추가 및 테스트 코드 수정
Geunbaek Dec 30, 2024
f8a7d79
Merge branch 'main' of https://github.com/Geunbaek/front_4th_chapter1-3
Geunbaek Dec 30, 2024
d7a7a49
chore: external test 스크립트 추가
Geunbaek Dec 30, 2024
2366a47
chore: external test ci 추가
Geunbaek Dec 30, 2024
0b59347
refactor: 사용되지 않는 폴더 삭제
Geunbaek Dec 30, 2024
6954f60
fix: 테스트 스크립트 수정
Geunbaek Dec 30, 2024
6d0319f
fix: ci 스크립트 수정
Geunbaek Dec 30, 2024
dc9e3ba
Merge branch 'main' into main
Geunbaek Jan 1, 2025
847b898
fix: git action 파일 분리
Geunbaek Jan 1, 2025
75c48c6
Merge branch 'main' of https://github.com/Geunbaek/front_4th_chapter1-3
Geunbaek Jan 1, 2025
735bf34
chore: import alias 설정 추가
Geunbaek Jan 2, 2025
43c5590
feat: types 추가
Geunbaek Jan 2, 2025
e8b9ff2
feat: enhanced 폴더 추가 ( 컨텍스트를 action 과 state 로 분리한 버전)
Geunbaek Jan 2, 2025
25c11fe
feat: enhancd 테스트 코드 추가 및 ci 추가
Geunbaek Jan 2, 2025
5fedabf
refactor: app
Geunbaek Jan 2, 2025
cbaa7e9
refactor: app-plus
Geunbaek Jan 2, 2025
372d821
feat: 테스트 코드 변경 및 스토어 생성방식 변경
Geunbaek Jan 2, 2025
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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,4 @@ jobs:
run: |
npm install
npm run test:advanced

34 changes: 34 additions & 0 deletions .github/workflows/ci2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: External Test

on:
pull_request:
types:
- synchronize
- opened
- reopened

workflow_dispatch:

jobs:
external:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: external-test
run: |
npm install
npm run test:external
enhanced:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: enhanced-test
run: |
npm install
npm run test:enhanced
15 changes: 12 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,22 @@
"test": "vitest",
"test:basic": "vitest basic",
"test:advanced": "vitest advanced",
"test:enhanced": "vitest enhanced",
"test:external": "vitest externalStore",
"test:ui": "vitest --ui",
"prepare": "husky"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"bash -c 'tsc --noEmit'",
"prettier --write",
"eslint --fix"
]
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@playwright/test": "^1.49.1",
Expand All @@ -32,10 +41,10 @@
"@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 All @@ -46,4 +55,4 @@
"vite": "^5.4.1",
"vitest": "^2.1.2"
}
}
}
32 changes: 31 additions & 1 deletion src/@lib/equalities/deepEquals.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
export function deepEquals<T>(objA: T, objB: T): boolean {
return objA === objB;
// 원시타입의 값이 동일하거나 기존 객체의 주소값이 바뀌지 않은 경우
if (objA === objB) {
return true;
}

// null인 경우
if (objA === null || objB === null) {
return objA === objB;
}

// 나머지 원시값들 처리
if (typeof objA !== "object" || typeof objB !== "object") {
return objA === objB;
}

const objAKeys = Object.keys(objA);
const objBKeys = Object.keys(objB);

if (objAKeys.length !== objBKeys.length) {
return false;
}

for (const key of objAKeys) {
const valueA = Reflect.get(objA, key);
const valueB = Reflect.get(objB, key);
if (!deepEquals(valueA, valueB)) {
return false;
}
}

return true;
}
32 changes: 31 additions & 1 deletion src/@lib/equalities/shallowEquals.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
export function shallowEquals<T>(objA: T, objB: T): boolean {
return objA === objB;
// 원시타입의 값이 동일하거나 기존 객체의 주소값이 바뀌지 않은 경우
if (objA === objB) {
return true;
}

// null인 경우
if (objA === null || objB === null) {
return objA === objB;
}

// 나머지 원시값들 처리
if (typeof objA !== "object" || typeof objB !== "object") {
return objA === objB;
}

const objAKeys = Object.keys(objA);
const objBKeys = Object.keys(objB);

if (objAKeys.length !== objBKeys.length) {
return false;
}

for (const key of objAKeys) {
const valueA = Reflect.get(objA, key);
const valueB = Reflect.get(objB, key);
Comment on lines +25 to +26
Copy link

Choose a reason for hiding this comment

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

Reflect라는게 있는걸 이번에 처음알았네요..! 정말 몰라서 여줍는건데, 혹시 이렇게 하는 것과, objA[Key]와는 어떤 차이가 있는지 여쭤봐도 괜찮을까요...?

정말 몰라서 신기해서 여쭙습니다..

Comment on lines +25 to +26

Choose a reason for hiding this comment

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

저는 Reflect가 Proxy 객체를 함께 사용할 때 객체의 특정 키에 접근할 때 의도하는 로직을 실행할때 사용하는 것으로만 알다가 실제 사용은 근백님 덕분에 알아갑니다 bb 다양하게 시도해서 코드를 간결하게 구현하신게 너무 좋습니다 bb

if (valueA !== valueB) {
return false;
}
}

return true;
}
13 changes: 10 additions & 3 deletions src/@lib/hocs/memo.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { shallowEquals } from "../equalities";
import { ComponentType } from "react";
import React, { ComponentType, ReactNode } from "react";

export function memo<P extends object>(
Component: ComponentType<P>,
_equals = shallowEquals,
) {
return Component;
let memoizedComponent: ReactNode | null = null;
let memoizedProps: P | null = null;
return (props: P) => {
if (!_equals(memoizedProps, props)) {
memoizedProps = props;
memoizedComponent = React.createElement(Component, props);
}
return memoizedComponent;
};
}
Comment on lines +8 to +16

Choose a reason for hiding this comment

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

크.. 👍 useRef 사용 안 하고 클로저를 사용해서 푸셨다니!

Copy link
Author

Choose a reason for hiding this comment

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

그런데 멘토링때 들은 바로는 위와같이 클로저를 이용하여 구현한 경우에는 메모리 누수가 발생할 수 있고 그것을 개발자가 따로 신경써 처리해야하는데 useRef를 이용해 구현하면 메모리 관리를 react 측으로 넘길 수 있다는 장점이 있다고 하셨습니다!

1 change: 1 addition & 0 deletions src/@lib/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./useDeepMemo";
export * from "./useMemo";
export * from "./useCallback";
export * from "./useRef";
export * from "./useStore";
8 changes: 5 additions & 3 deletions src/@lib/hooks/useCallback.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/* 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<T extends Function>(
factory: T,
_deps: DependencyList,
) {
// 직접 작성한 useMemo를 통해서 만들어보세요.
return factory as T;
// eslint-disable-next-line react-hooks/exhaustive-deps
const memoizedCallback = useMemo(() => factory, _deps);
return memoizedCallback;
}
1 change: 0 additions & 1 deletion src/@lib/hooks/useDeepMemo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@ import { useMemo } from "./useMemo";
import { deepEquals } from "../equalities";

export function useDeepMemo<T>(factory: () => T, deps: DependencyList): T {
// 직접 작성한 useMemo를 참고해서 만들어보세요.
return useMemo(factory, deps, deepEquals);
}
13 changes: 10 additions & 3 deletions src/@lib/hooks/useMemo.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
/* 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 valueRef = useRef<T | null>(null);
const depsRef = useRef(_deps);

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

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

export function useRef<T>(initialValue: T): { current: T } {
// React의 useState를 이용해서 만들어보세요.
return { current: initialValue };
const [ref] = useState(() => ({ current: initialValue }));
return ref;
}
16 changes: 16 additions & 0 deletions src/@lib/hooks/useStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useRef } from "./useRef";
import { useSyncExternalStore } from "react";
import { shallowEquals } from "../equalities";
import { Store } from "../../storeUtils";

export const useStore = <T, S>(store: Store<T>, selector: (store: T) => S) => {
const prevRef = useRef<S | null>(null);

return useSyncExternalStore(store.subscribe, () => {
const next = selector(store.getState());
if (prevRef.current === null || !shallowEquals(prevRef.current!, next)) {
prevRef.current = next;
}
return prevRef.current;
});
};
Comment on lines +9 to +16
Copy link

Choose a reason for hiding this comment

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

리뷰하러 왔다가 배우고만 가는 것 같네요.. useSyncExternalStore를 이렇게 활용하는구나를 배우고 갑니다..! 👍

Copy link

Choose a reason for hiding this comment

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

한가지 궁금한게 있어서 여쭙습니다. useStore에서 전역 공간 확보를 위해서 useSyncExternalStore를 쓰신 것 같은데 혹시 제가 이해한게 맞을까요...?
또한, useContext 같은 방법으로도 전역 상태를 관리할 수 있다고 생각하는데.. 혹시 이 훅을 사용하신 이유 있으신지 궁금합니다..

생소한 훅이라서 정말 궁금하여 여쭙습니다...!

Choose a reason for hiding this comment

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

오... useSyncExternalStore은 뭔가요??

Choose a reason for hiding this comment

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

저도 useSyncExternalStore가 뭔지 궁금하네요,, 주로 전역으로 관리하기 위해 사용되는 건가요??? 어떨때 주로 사용되나용?

Copy link
Author

@Geunbaek Geunbaek Jan 4, 2025

Choose a reason for hiding this comment

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

오... useSyncExternalStore은 뭔가요??
저도 useSyncExternalStore가 뭔지 궁금하네요,, 주로 전역으로 관리하기 위해 사용되는 건가요??? 어떨때 주로 사용되나용?

useSyncExternalStore는 외부 데이터 저장소와 React 컴포넌트를 동기화하는데 사용하는 훅입니다.

const state = useSyncExternalStore(
  subscribe,  // 구독 함수
  getSnapshot,  // 현재 상태를 반환하는 함수
  getServerSnapshot  // (선택적) 서버 렌더링용 초기 상태
);

또한, useContext 같은 방법으로도 전역 상태를 관리할 수 있다고 생각하는데.. 혹시 이 훅을 사용하신 이유 있으신지 궁금합니다..

context api 와 함께 이용한 이유는 context api 를 사용해 지역적인 상태들을 관리하곤 하는데 이와 전역상태와 구분하기 위해 사용합니다. ( 해당 context 내부에서만 해당 상태를 사용할 수 있도록 )

저는 zustand를 자주 사용하고 있어서 진짜 전역적인 변경이 있는 경우 zustand 를 그 외에는 context api + zustand를 사용합니다.
위와 같이 함께 사용하는 이유는 context api 만을 사용하면 따로 최적화 해주는 과정이 필요한데 ref를 통해 store를 바라보고 selector 를 통해 필요한 상태만 뽑아쓰는 구조를 사용하면 별다른 최적화 없이 같은 스토어라도 해당 상태를 사용하는 컴포넌트만 업데이트 할 수 있기 때문입니다 !

Loading
Loading