과제 체크포인트
배포 링크
https://eveneul.github.io/front_6th_chapter1-3/
기본과제
equalities
- shallowEquals 구현 완료
- deepEquals 구현 완료
hooks
- useRef 구현 완료
- useMemo 구현 완료
- useCallback 구현 완료
- useDeepMemo 구현 완료
- useShallowState 구현 완료
- useAutoCallback 구현 완료
High Order Components
- memo 구현 완료
- deepMemo 구현 완료
심화 과제
hooks
- createObserver를 useSyncExternalStore에 사용하기 적합한 코드로 개선
- useShallowSelector 구현
- useStore 구현
- useRouter 구현
- useStorage 구현
context
- ToastContext, ModalContext 개선
과제 셀프회고
기술적 성장
1주차부터 3주차까지 자바스크립트를 나름대로 딥 다이브 한 것 같다. 프레임워크 없이 SPA 만들기는 정말 생각하지도 못했고, 리액트를 사용하면서 이건 자바스크립트로 어떻게 굴러가는 거지? 라는 생각을 개발 시장에 뛰어든 지 3년에서야 하게 되었다. 이것 참 부끄러운 일이다..
각설하고, 기술적인 성장을 나열하자면 아래와 같다.
- [얕은 비교/깊은 비교]
Object.is를 처음 봤다.
동등 연산자, 일치 연산자와의 차이도 알았다. 대신 이해는 조금 어려웠다. 타입까지 분별해 주기에 일치 연산자를 쓰라고 권장하기에 일치 연산자가 만능인 줄 알았다. 그런데 그게 객체에서는 적용이 안 되는 줄은 몰랐다.
객체는 참조형 타입이므로 일치 연산자로 비교를 했을 때, 메모리 주소값으로 비교를 하기 때문에 key-value값을 확인하지 못하기에 Object.keys를 통해 반복문을 돌면서 Object.is로 key, value가 같은지 확인했다.
const keysA = Object.keys(a as Record<string, unknown>);
const keysB = Object.keys(b as Record<string, unknown>);
// 길이가 다르면 false 리턴
if (keysA.length !== keysB.length) {
return false;
}
// A 키를 기준으로 B에 같은 키가 있는지, 그리고 그 값이 있는지 확인
// Object.prototype.hasOwnProperty.call() => 객체가 해당 속성을 직접 소유하고 있는지 확인
for (let i = 0; i < keysA.length; i++) {
const currentKey = keysA[i];
if (
!Object.prototype.hasOwnProperty.call(b, currentKey) ||
!Object.is((a as Record<string, unknown>)[currentKey], (b as Record<string, unknown>)[currentKey])
) {
return false;
}
}
Object.prototype.hasDownProperty.call로 B 객체에 A가 가지고 있는 키가 없거나, Object.is로 두 값의 key가 일치하지 않으면 두 오브젝트는 같지 않다고 반환해 주었다.
- [useRef] useState 초기값에 왜 { current: initialValue }를 넣으면 안 될까?
useRef는 렌더링에 필요하지 않은 값을 참조할 수 있다. initialValue에 값을 넣으면 초기 렌더링 이후부터는 무시된다. 같은 값을 유지해야 하는 방법은 아래와 같다.
- useState: 상태 유지
- useRef: 참조 유지 (그런데 이걸 만들어야 하니까 쓸 수 없음)
- useMemo: 계산 결과 유지
- useReducer: 복잡한 상태 유지
useMemo는 복잡한 계산을 캐싱해서 성능을 최적화시키는 건데, 생각보다 많은 데에 쓰일 useRef를 만들 때 useMemo를 쓰는 게 맞나? 싶었다. 잘못 사용하면 성능이 더 안 좋아진다. useReducer는 너무 복잡했다. 그래서 useState로 만들었다.
그런데 한 가지 더 의문이 생겼다. 왜 useState({ current: initialValue })는 안 되고, useState(() ⇒ { current: initialValue })는 되는 것일까?
useState에게 직접적으로 부여한 객체(값)은 랜더링 될 때마다 생성하고, 함수는 첫 랜더링 이후 다시 실행되지 않는다. 이 점이 useRef를 구현하는 데에 잘 맞는 것 같아서 useState를 사용했다.
function App() {
const [count, setCount] = useState(0);
console.log('🔥 컴포넌트가 실행되었습니다.');
console.log('🏃 직접 객체 코드 실행 시작');
const directObject = { current: 100 };
console.log('🏃 직접 객체 생성됨::', directObject);
console.log('👀 함수 코드 실행 시작');
const [ref] = useState(() => {
console.log(
'🍀🍀🍀🍀🍀🍀🍀🍀 함수 내부 실행됨. 새 객체 생성 🍀🍀🍀🍀🍀🍀🍀🍀🍀'
);
return { current: 200 };
});
console.log('🫵 useState 완료 (직접 객체도, 함수 코드도)');
return (
<div className='App'>
<span>개발자 도구 콘솔창을 열어서 결과를 확인해 보세요.</span>
<button onClick={() => setCount(prev => prev + 1)}>
리렌더링 시키기!
</button>
</div>
);
}
결과는 여기에서 확인 가능하다. https://2473697.playcode.io/ (수민 공주.. 보고 있어..?)
- useSyncExternalStore Hook에 대해서
리액트에서 전역 상태 관리는 Recoil, Zustand 같은 라이브러리를 사용해서 전역 상태에 대한 깊은 생각을 하지 않았었는데, 이번 과제를 통해 useSyncExternalStore 훅도 보고, 라이브러리를 직접 뜯어 볼 수 있어서 재미있었다. 관련한 내용은 벨로그를 통해 포스트 발행했다.
자랑하고 싶은 코드
// createObserver.ts
export const createObserver = () => {
const listeners = new Set<Listener>();
// useSyncExternalStore 에서 활용할 수 있도록 subscribe 함수를 수정합니다.
const subscribe = (fn: Listener) => {
listeners.add(fn);
return () => {
listeners.delete(fn);
};
};
const notify = () => listeners.forEach((listener) => listener());
return { subscribe, notify };
};
사실 자랑하고 싶은 잘 짠 코드는 아니고, 내가 이해해서 과제를 수행했다! 를 보여 드리고 싶다. 실은 그간 보냈던 2주 동안에 listener에 대한 깊이 있는 이해가 부족해서 store를 구현하는 데에도 이게 왜 되지? 싶었는데, 이번 주차 과제를 통해 에라이~ 모르겠다! 하고 listener에 대해 좀 찾아봤다.
unsubscribe 함수를 삭제하고 subscribe 안에 return () ⇒ { … }로 추가했다. Zustand 같은 라이브러리를 사용한다면 알아서 메모리 누수를 방지하기 위해 구독 해제도 해 주지만, 생으로 store를 만들 때에는 다른 함수로 빼는 것이 아니라 같은 구독 함수 내에서 반환해 주어야 메모리 누수가 생기지 않는다.
개선이 필요하다고 생각하는 코드
개선이 필요한 건 아니고, 왜 이렇게 돌아가는지 궁금했지만 (원래는 개인 회고 질문란에 넣을 생각이었다) 금주 준일 코치님 멘토링 시간에 해답을 찾았다.
const defaultSelector = <T, S = T>(state: T) => state as unknown as S;
export const useRouter = <T extends RouterInstance<AnyFunction>, S>(router: T, selector = defaultSelector<T, S>) => {
// useSyncExternalStore를 사용하여 router의 상태를 구독하고 가져오는 훅을 구현합니다.
const shallowSelector = useShallowSelector(selector);
// useSyncExternalStore 두 번째 인자(현재상태)는 "전체 상태"를 반환해야 함
const state = useSyncExternalStore(router.subscribe, () => shallowSelector(router));
return state;
};
useSyncExternalStore의 두 번째 인자로 현재 상태값을 넣어 줘야 하는 건 이해했다. 그런데 왜 router.pathname을 넣는 게 아닐지 궁금했다. 현재 상태값이라고 하면, 즉 router의 현재 상태값이라고 하면 pathname이 맞다고 생각했는데, 코치님께서는 아래와 같은 답변을 주셨다.
pathname만 넘겨줘도 좋을 것 같긴 한데, 문제는 router 자체 대한 정보가 다 필요할 때가 있어서,
pathname 뿐만 아니라 router 자체를 넘겨줘야 하지 않나!? 라고 생각이 되네요..!
이걸 듣고 앗.. 했다. 실제 (내가 사용할 때) 리액트에서 pathname을 얻을 때 const router = useRouter()로 선언해 준 뒤, router.pathname으로 접근하기도 하고, pathname만 얻는 거면 새로운 usePathname() 같은 걸 만들어 줘야 하지 않나? 결론은 닉값 하지 못한다. 라고 생각이 되었다.
학습 효과 분석
- 새로운 문법? 같은 걸 많이 만나 볼 수 있어서 좋았다. 앞서 작성했던 Object.is라든가, hasOwnProperty라든가, useSyncExternalStore 등..
- 스토어를 만들 때 왜 listener가 필요한지도 깊게 파고들 기회가 생겨서 좋았다. 그리고 일반 배열이 아닌 new Set으로 중복 방지를 해 주는 것까지!
- 실무 적용 가능성은 잘 모르겠다. 회사 바이 회사, 실무 바이 실무일 것 같다. 저희 회사는 개발 일감이 없어요.. 하.. 하지만 다른 프로젝트를 하거나, 다른 회사로 이직하게 된다면 useMemo, useCallback은 한두 번 정도 써먹을 수 있을 것 같다.
과제 피드백
- 1, 2주차보다 재미있었습니다. 1주차는 UI를 직접 조작할 수 있었지만 첫 과제라는 부담감과 자바스크립트로 리액트를 직접 구현한 적이 없어서 어려웠고, 2주차는 알고리즘 성격의 로직 구현이 있어서 제 뇌로는 한계가 있는 것 같았지만.. 1, 2주차로부터 얻은 (얕은) 지식으로 충분히 풀어나갈 수 있어서 일찍 과제를 마치고(제일 중요) 저녁 약속도 잡았습니다.(이게 더 중요) 🍺
- 결론: 1, 2주차를 경험했어서, 3주차 과제가 수월할 수 있었습니다.
학습 갈무리
리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.
- 리액트는 Virtual DOM(가상 돔)을 만들어서 이전 Virtual DOM 트리랑 비교(Diffing)하여 바뀐 부분을 실제 DOM에 반영(Reconciliation)한다.
- 그렇다면 렌더링이 일어날 때는 아래와 같다.
- 상태가 바뀔 때 (useState로)
- props가 바뀔 때, Context가 바뀔 때
- 부모가 리랜더링되면서 자식도 덩달아 부모 컴포넌트가 바뀌면 자식들도 “어? 나도 바뀌어야 하나?” 하면서 리랜더링 (이게 중요)
- …
- 여기에서 중요한 점은 나는 부모만 바꾸고 싶은데 자식까지 같이 바뀌어서 개발자가 가늠하지 못한 리랜더링이 일어난다는 것이다.
- 만약 자식 컴포넌트가 무거운 계산을 가지고 있다면, 부모가 바뀔 때마다 (말이 조금 이상하지만) 다시 계산을 해 주어야 하니 난감한 상황이 발생한다.
- 그럴 때 무거운 계산을 수행해야 하는 컴포넌트에게 useMemo, useCallback, React.memo 등으로 복잡한 계산을 캐싱해 준다.
useMemo: 복잡한 계산이 끝난 뒤 ‘값’을 메모이제이션한다.useCallback: 함수 자체를 메모이제이션해서 불필요한 함수 재생성을 방지.React.memo: 컴포넌트 자체를 메모이제이션한다.
- 이러한 메모이제이션 훅을 이용해서 이전 값과 현재 값이 같으면 리랜더링하지 않는다. 하지만 메모이제이션도 비용이므로 꼭 필요한 곳에만 사용한다.
메모이제이션에 대한 나의 생각을 적어주세요.
- 그렇다면 메모이제이션은 언제 필요할까? 우선 메모이제이션이 무엇일까?
메모이제이션: 한 번 계산한 건 메모해 뒀다가(RAM(메모리)에 저장해 두었다가) 똑같은 계산을 해야 한다면, 메모리에 저장해 둔 걸 다시 재사용하는 것.
- 말만 들으면 오?! 이러면 죄다 저장하면 개꿀이잖아 ㅋㅋ 하겠지만.. 램은 한정되어 있고, 컴퓨터에 있는 램이 온전히 개발할 때 사용할 수도 없어서 너무 많이 저장하면 메모리 부족으로 컴퓨터가 느려진다.
- 즉, 메모이제이션은 꼭 필요한 때에 사용하라고 한다. 그러라고 하지만 아직까지 나는 메모이제이션을 어떻게 잘 사용해야 할지 감이 안 잡힌다. AI에게 물어봐도 10000을 반복문으로 돌렸을 때… 라는 예시만 줄 뿐이다.
컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.
- 컨텍스트는 또 뭘까? 개발하다 보면 컨텍스트, 컨텍스트 한다.
한국인이면 모국어를 써라.컨텍스트: 지금 어떤 상황인지, 어떤 환경인지 나타내는 정보. 리액트에서는 대다수의 컴포넌트에서 필요로 하는 값을 저장하는 저장소. (정확한가?!)
- 그러면 컨텍스트와 상태 관리는 같은 말일까?
아니다!리액트에서 컨텍스트는 도구 같은 개념이고, 상태 관리는 목적? 개념적으로 더 큰 의미를 가진다. 즉 컨텍스트는 상태 관리 도구, 상태 관리는 방법론이다.
- 컨텍스트와 상태 관리는 왜 필요할끼?
- props drilling을 방지하기 위해서다. A 컴포넌트에서 버튼을 누르면 전혀 다른 부모를 가진 B 컴포넌트에서 리랜더링이 일어나야 하는데, props로 옮겨 주려면 A 컴포넌트 자식으로 B가 있어야 하는데.. 애초에 말이 안 된다.
- 주의해야 할 점은, Context Provider로 감싼 컴포넌트들은 Context의 상태가 변경될 때 리랜더링 된다는 것이다. 그래서 dark / light 모드처럼 정말 모든 컴포넌트에 영향이 가야 하는 케이스일 때만 Context를 사용해 주는 게 좋을 것 같다.
리뷰 받고 싶은 내용
- useMemo, useCallback, React.memo의 구체적인? 실무적인? 예시를 모르겠습니다. 단순한 CRUD를 주로 만져서 그런지 메모이제이션을 쓸 일이 많이 없었는데요, 코치님께서는 작업을 하시다가 메모이제이션을 써야겠다! 라는 감이 올 때가 있으신가요?
과제 피드백
안녕하세요 하늘님! 3주차 과제 잘 진행해주셨네요 ㅎㅎ 고생하셨습니다!
실은 그간 보냈던 2주 동안에 listener에 대한 깊이 있는 이해가 부족해서 store를 구현하는 데에도 이게 왜 되지? 싶었는데, 이번 주차 과제를 통해 에라이~ 모르겠다! 하고 listener에 대해 좀 찾아봤다.
블로그 포스트까지 작성해주시고! 너무 고생하셨어요~ 저도 하늘님의 글을 보면서 부족한 지식을 채워나갈 수 있었답니다!
결론: 1, 2주차를 경험했어서, 3주차 과제가 수월할 수 있었습니다.
의도했던 부분은 아니지만, 효과가 있었다니 다행이네요!! 감사합니다 ㅎㅎ
useMemo, useCallback, React.memo의 구체적인? 실무적인? 예시를 모르겠습니다. 단순한 CRUD를 주로 만져서 그런지 메모이제이션을 쓸 일이 많이 없었는데요, 코치님께서는 작업을 하시다가 메모이제이션을 써야겠다! 라는 감이 올 때가 있으신가요?
무조건 이런 상황에는 써야돼! 라기 보단, 제일 좋은건 "필요한 순간"에 적용하는거라고 생각해요. 그렇다면 필요한 순간이 언제일까? 에 대한 판단을 해야될텐데, 이건 비즈니스와 관련이 있답니다. 내가 만드는 서비스의 사용성에 문제가 있을 때를 인지한 시점이라고 생각해요. 그 이전까지는 개선을 한다고 해도 개발자 개인의 욕심이지 않을까!? 싶어요 ㅎㅎ
조금 더 수치적으로 이야기해보자면, 인터렉션에 의한 1회 렌더링이 200ms 이상 소모될때!? 라고 해야하나.. 그렇습니다.
이에 대한 부분은 9~10주차 과제에서 체험해볼 수 있으니 조금만 더 기다려주세요! 혹은 "렌더링이 많이 발생하는 상황"을 AI에게 만들어달라고 하고, 해당 코드에 대해 직접 리팩토링을 해보는거죠!