과제 체크포인트
배포 링크
bebusl.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 개선
과제 셀프회고
평상시에 쓰던 훅/hoc를 일부 구현해보면서, 그 훅들의 원리에 대해 좀 더 잘 알수 있어서 좋았습니다. useAutoCallback은 실제 코드에서도 요긴하게 쓰일만한 훅이어서 가져다가 써보려고 합니다. 과제 해결 자체는 빨리 했는데 글쓰기(PR/블로그 등)를 회피해서 또 PR쓰기를 미루고 미루고 미뤄서 자괴감이 들었습니다. 남은 항해를 진행하면서 회고를 제때 남기는 버릇 + 그때그때 생각난 글감을 한 곳에 아카이브하는 습관을 만들도록 노력해야할 것 같습니다.
학습 효과 분석 및 기술적 성장
-
Fiber에 대한 학습
- useRef 구현하다 보니, 리렌더링이 될때 컴포넌트가 재실행되면서 컴포넌트 내부의 계산들은 다 새로 될텐데, useRef는 재실행됨에도 어떻게 다시 초기화되지 않고 이전 값을 유지할까? 그러고 보니 state에 대한 것도 어딘가엔 저장할텐데 어디에 저장되는거고 어떻게 관리되는 걸까 ?궁금증이 생겼습니다.
- 이 궁금증을 해결하기 위해선 react 16부터 도입된 Fiber라는 아키텍처를 봐야했습니다..
- fiber는 type, pendingProps, memoizedProps, memoizedState, stateNode, …으로 구성되어 있습니다.
- 이 memoizedState에 연결 리스트 형식으로 useState, useReducer, useRef등의 hook 상태가 저장됩니다.
- 재렌더링 시에 기존 fiber가 버려지는 것이 아니라, 기존 fiber트리를 참조해서 새로운 fiber 트리 구성합니다. → 결론적으로 이전 데이터를 기반으로 새 fiber가 만들어지므로 기존의 hook의 결과물들이 기억된다고 보면 됩니다.
- fiber는 우리가 2주차 과제에서 구현했던 vNode와 구조적으로 비슷하게 대응된다고 보면 됩니다.(다만 fiber는 UI구조 뿐만 아니라 렌더링 상태, 업데이트 큐, hook 상태, 우선순위 같은 추가 정보까지 담겨있는 복합 구조체라는 차이는 있음.)
-
메모이제이션 관련된 hook/hoc에 대한 이해도 상승, 다른 커스텀 훅을 만들 때도 좀 더 정확히 이해하고 활용할 수 있을 것 같습니다.
-
useCallback(fn, [])처럼 deps 없이 고정시키면 오래된fn을 계속 참조해서 버그가 생길 수 있는데,useRef를 함께 쓰면 참조값은 유지하면서도 최신 함수 로직을 안전하게 반영할 수 있다는 깨달음을 얻었습니다.이 패턴은 특히 하위 컴포넌트에 콜백 props를 넘기거나, 외부 라이브러리 콜백 등록 시 유용할 것 같고 함수 재생성으로 인한 불필요한 렌더링 문제를 해결할 수 있어 실무에서 유용하게 사용할 것 같습니다.
-
-
pnpm을 이용한 모노레포 슬쩍 엿보기
- 모노레포 구조를 슬쩍 엿본것만으로도 꽤 유익한 경험이었습니다.
- 단순히 이론으로 보는 것보다, 실제 구조를 훑어보는 것만으로도 더 빠른 이해가 가능했던 것 같습니다. 역시 이론 + 실습이 최고라는 걸 깨닫고 다른 공부할 때도 너무 이론에만 몰두하지 않도록 해야겠단 생각이 들었습니다.
자랑하고 싶은 코드
equals함수를 구현할 때 object형식인지 판별해야하는 곳이 있었는데, parameter자체는 unknown으로 정의되어 있었습니다.
이 부분을 typescript의 is 키워드를 이용해 좀 더 안전하게 타입 좁히기를 하면 좋겠다는 생각을 하였고, 아래와 같이 적용했습니다.
// 타입가드 함수
export const isObject = (a: unknown): a is { [k: PropertyKey]: unknown } => {
return a !== null && typeof a === "object";
};
// 적용부
const equals = (a:unknown, b:unknown) => {
const isAObject = isObject(a);
const isBObject = isObject(b);
if (isAObject && isBObject) {
... 이 안에서 a와 b는 {[k:PropertyKey]:unknown}으로 추론됨
}
}
개선이 필요하다고 생각하는 코드
마찬가지로 equals함수 관련된 부분이었는데요, shallowEquals, deepEquals 둘이 코드 베이스가 거의 똑같은데 그 부분을 따로 추출하여 쓰면 더 좋았을 것 같습니다.
(memo → deepMemo같은 구조로 갈 수 있었으면 좋았을 것 같습니다)
과제 피드백
hook/hoc 구현을 하는데 흐름이 자연스럽게 짜여있어서 이해하기에 좋았던 것 같습니다. 다음 hook은 어떻게 만들면 되겠구나, 저건 어떤 개념을 활용하면 되겠구나!가 자연스럽게 떠오르게 흐름이 잘 구성된 것 같습니다.
학습 갈무리
리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.
- 컴포넌트 함수를 실행한다.
- createElement가 호출되고 가상DOM트리에 필요한 정보를 가진 ReactElement가 만들어집니다.
- 가상 DOM 생성 및 비교(Render단계, Reconciliation)
- 가상 DOM 트리를 구성합니다.
- Diffing 알고리즘을 통해 어떤 부분이 바뀌었는지 확인하고 변경된 부분만 추려냅니다.
- 실제 돔 업데이트를 한다.(Commit 단계)
- 변경된 부분을 브라우저 DOM에 실제로 반영합니다.
- 변경된 부분 리스트(Effect list)를 순회하면서 실제 DOM 조작을 실행합니다.
- 이 때 어딜 바꿔야 할 지 이미 정해져있으니 효율적인 DOM 조작이 가능해집니다.
- useEffect나 ref 등도 이 시점에 실행이 됩니다.
- 변경된 부분을 브라우저 DOM에 실제로 반영합니다.
메모이제이션에 대한 나의 생각을 적어주세요.
다른 곳에서도 성능 최적화를 위해 사용될 수 있겠지만 실시간성이 중요한 서비스, 예를 들어 라이브 방송처럼 여러 명의 유저가 동시에 상호작용하며 UI가 빈번하게 바뀌는 환경에서는 메모이제이션이 매우 효과적으로 활용될 수 있을 것 같습니다.
실시간 동영상 스트리밍과 같은 경우에 초 단위로 UI 갱신이 발생하므로 동일한 입력에 대해 중복 렌더링이나 연산을 피하는 것이 성능 유지에 중요합니다.
따라서 사용자 상호작용이 잦고 렌더링이 빈번히 발생하는 실시간 서비스에서 메모이제이션을 적극 활용하면 성능을 안정적으로 유지하는 데 큰 도움이 된다고 생각합니다.
컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.
컨텍스트는 상태나 데이터를 관심사에 따라 분리해서 저장해두는 공간이라고 생각합니다.(예시: 테마 정보는 ThemeContext에, 사용자 정보는 UserContext))
컨텍스트를 이용하면 전역적인 정보를 한 곳에서 제공할 수 있어서 불필요한 props drilling을 막을 수 있습니다. 만약 컨텍스트를 사용하지 않고 일일이 단계별로 데이터를 내려준다면 추적이 매우 힘들어지고 코드도 복잡해질 것입니다.
상태관리는 UI에 영향을 주는 여러 사용자 상호작용이나 비동기 데이터 변화를 효과적으로 관리하는 작업이라고 생각합니다. 상태가 바뀔 때마다 UI가 원하는 대로 정확하게 반영되어야 하기 때문에 상태를 어디에, 어떻게 저장할지에 대해 정하는 것가 매우 중요하다고 생각합니다.
또한 상태관리는 단순히 값을 저장하는 것뿐만 아니라, 상태 변경의 흐름을 명확하게 만들고 예측 가능하게 만드는 과정이라는 점에서 개발자의 편의를 위한 중요한 작업입니다.
결국 우리가 직접 다루는 코드는 사람이 읽고 유지보수하는 것이기 때문에, 유지보수하기 쉽고 읽기 좋은 코드가 가장 중요하다는 생각입니다. 대부분의 기술이나 방법론은 바로 이 목표를 달성하기 위한 도구일 뿐이라고 느끼게 되었습니다.
리뷰 받고 싶은 내용
유틸 함수 분리 기준과 테스트 가능성 사이의 균형
유틸 함수를 작성할 때, 저는 150줄 미만 정도의 함수라면 굳이 세세하게 나누기보다는 기능 단위로 한 파일에 모아두는 방식을 선호하는 편입니다. 그런데 이번에 equals 함수를 직접 구현해보면서, 타입에 따른 분기 처리를 한 함수 안에서 모두 처리했는숩니다.
shallowEquals나 deepEquals 파일을 보시면 어레이 타입일때, 객체일때, 원시타입잍 때의 로직 모두 그냥 shallowEquals,deepEquals 안에 작성 되어 있는데 각 타입별 로직을 분리한 다음 equals함수에서 조립하는 식으로 분리하는 편이 더 좋았을 것 같기도 합니다.
근데 이런 조그마한 함수들이 너무 많아지면 오히려 관리가 더 힘들지 않을까하는 생각이 동시에 들기도 합니다.
코치님은 이런 경우, 어디까지를 "분리할 만한 단위"로 보시는지 의견이 궁금합니다.
명확함을 위해 긴 네이밍을 쓰는 게 좋을까요?
저는 변수나 함수 이름을 지을 때, isEnabledByUserAction, fetchPostsByCategoryId처럼다소 길어지더라도 형용사나 전치사(by 등)를 붙여서 더 명확하게 표현하는 걸 선호하는 편입니다. 그런데 코드 리뷰를 하다 보면, 길다는 이유로 이름을 더 간결하게 바꾸자는 피드백을 받는 경우도 있어서 고민됩니다. 코치님은 협업 코드에서 가독성과 간결성 사이의 네이밍 밸런스를 어떻게 잡으시나요? 그리고 긴 이름이더라도 명확하면 괜찮다고 보는지, 혹은 더 좋은 네이밍 전략이 있는지 궁금합니다.
과제 피드백
안녕하세요 진희님! 3주차 과제 잘 진행해주셨네요 ㅎㅎ 고생하셨씁니다!
코치님은 이런 경우, 어디까지를 "분리할 만한 단위"로 보시는지 의견이 궁금합니다.
저는 함수의 추상화 수준이 제일 중요하다고 생각합니다. 가령 equals 함수를 토대로 생각해보자면
if (원시타입) {
...
}
if (배열) {
...
}
if (객체) {
...
}
return false
차라리 이렇게 전체적으로 함수를 사용하는 모습이 아니라고 가정했을 때, 조금 도 융통성(?)이 있죠 ㅎㅎ 그런데 한 부분만 함수로 분리되어있다고 가정해보면
if (원시타입) {
...
}
if (이건배열임) { return 배열일때처리함() }
if (객체) {
...
}
return false
이러면 "저 구간은 왜 함수로 분리된거지?" 라는 의문이 생길 수 있어요.그래서 차라리 이렇게 전체적으로 함수로 분리해서 "추상화 수준"을 유지해야 한다고 생각합니다.
return pick(
[원시타입일때(), 원시타입처리()],
[배열일때(), 배열처리()],
[객채일때(), 객체처리()],
[전부다아니면False()]
)
대충 이런 모습이랄까..
아니면 타입가드만 함수로 분리하거나 혹은 각 타입에 대한 내부 구현을 함수로 분리하는 등 여러가지 방법이 있겠네요 ㅎㅎ
명확함을 위해 긴 네이밍을 쓰는 게 좋을까요? 저는 변수나 함수 이름을 지을 때, isEnabledByUserAction, fetchPostsByCategoryId처럼다소 길어지더라도 형용사나 전치사(by 등)를 붙여서 더 명확하게 표현하는 걸 선호하는 편입니다. 그런데 코드 리뷰를 하다 보면, 길다는 이유로 이름을 더 간결하게 바꾸자는 피드백을 받는 경우도 있어서 고민됩니다. 코치님은 협업 코드에서 가독성과 간결성 사이의 네이밍 밸런스를 어떻게 잡으시나요? 그리고 긴 이름이더라도 명확하면 괜찮다고 보는지, 혹은 더 좋은 네이밍 전략이 있는지 궁금합니다.
저는 함수가 쓰이는 맥락이 중요하다고 생각해요. 가령, 어디서든 독립적으로 쓰이는 함수의 경우 말씀해주신 것 처럼 구체적인게 좋지만, 네임스페이스나 훅을 통해 정의되는 함수는 이렇게 쓰일 수도 있겠죠!?
// 객체로 쓰일 때
userAction.isEnabled();
postsService.fetchByCategoryId();
// 혹은 훅으로 쓰일 때
const userAction = useUserAction();
const posts = usePosts();
userAction.isEnabled();
posts.fetchByCategoryId();
그런데 네임스페이스와 중복된다면 조금 읽는게 불편한다고 생각해요.
userAction.isEnabledByUserAction();
posts.fetchPostsByCategoryId();
그래서 이 함수가 어떤 맥락에서 쓰이는지를 토대로 구체성을 정의해야 한다고 생각한답니다!