[2팀 이진희] Chapter 1-3. 프레임워크 없이 SPA 만들기#7
Conversation
* state -> Toast를 포탈 태우는 곳에서만 알고 있으면 됨 * command -> 전역적으로 사용 state와 command가 서로 상태 변화 영향 받지 않도록 provider와 context를 완전 분리
devchaeyoung
left a comment
There was a problem hiding this comment.
주석 덕분에 코드가 잘 읽혔던 것 같아요! 이번주차 너무너무 고생하셨습니다!! PR작성까지 화이탱!!
| return a === b; | ||
| import { isObject } from "../utils/typeGuards"; | ||
|
|
||
| export const deepEquals = (a: unknown, b: unknown): boolean => { |
There was a problem hiding this comment.
deepEquals.ts와 shallowEquals.ts는 로직이 일관성있어서 가독성이 좋은 것 같습니다!
혹시 더 유지보수성을 높이고 싶으시다면, 공통 로직을 baseEquals와 같은 함수로 분리해서,
비교하는 인자만 넘기는 방식으로 리팩토링해보는 것도 추천드려요 !
예를 들면
type CompareFn = (a: unknown, b: unknown) => boolean;
function baseEquals(
a: unknown,
b: unknown,
valueCompare: CompareFn // 값 비교 전략 (===, deepEquals 등)
): boolean {
const isAObject = isObject(a);
const isBObject = isObject(b);
if (!isAObject && !isBObject) return Object.is(a, b);
if (Array.isArray(a) !== Array.isArray(b)) return false;
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
return a.every((item, idx) => valueCompare(item, b[idx]));
}
if (isAObject && isBObject) {
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) return false;
for (const key of aKeys) {
if (!valueCompare(a[key], b[key])) return false;
}
return true;
}
return false;
}이렇게 baseEquals가 있다고 했을 때
export const shallowEquals = (a: unknown, b: unknown) =>
baseEquals(a, b, (x, y) => x === y);
export const deepEquals = (a: unknown, b: unknown): boolean =>
baseEquals(a, b, deepEquals);이런 식으로 사용할 수 있을 것 같아요!!
| const isObject = (a: unknown): a is Record<string, unknown> => { | ||
| return a !== null && typeof a === "object"; | ||
| }; | ||
|
|
There was a problem hiding this comment.
오, 저는 if문에서 처리했는데, 이렇게 함수로 뺀거 좋은거같아용 👍
| // useState를 사용하여 상태를 관리하고, shallowEquals를 사용하여 상태 변경을 감지하는 훅을 구현합니다. | ||
| return useState(initialValue); | ||
| const [state, _setState] = useState<T | undefined>(initialValue); | ||
| const curState = useRef<T>(state); //setState 메모이제이션용. state를 의존해서는 안됨. -> 매번 리렌더링 됨 |
There was a problem hiding this comment.
useRef 를 사용해 state를 저장한 이유가 매번 리렌더링 되는걸 막기 위해서인가요!? 매번 리렌더링이 되는 이유가 궁금합니다!
There was a problem hiding this comment.
제가 만든 setState 함수에서 state를 직접 참조하면, state가 변경될 때마다 의존성 배열에 state가 들어가게 되고, 그 결과 state가 업데이트될 때마다 setState가 매번 새로 생성됩니다.
그래서 useRef를 사용해 상태를 관리함으로써 setState 함수가 불필요하게 재생성되는 걸 막고 동시에 최신 상태에 안정적으로 접근할 수 있게 했습니다.
제가 '리렌더링'이라고 표현을 좀 잘못 사용한 것 같아요!
리렌더링을 막기 위함이 아니라 setState함수가 매번 재생성되는 걸 막기 위해 ref를 썼다고 보심 될 것 같아요
| "pnpm": ">=10" | ||
| }, | ||
| "type": "module", | ||
| "homepage": "https://bebusl.github.io/front_6th_chapter1-3/", |
There was a problem hiding this comment.
앗 별다른 기능이 있는건 아닌데 gh-pages로 배포할 때 homepage 적어주면 리소스 경로 오류 방지해준대서 넣어뒀어요
gpt에 따르면 이렇다네요; 저도 그냥 버릇처럼 넣은거라 빼고 한 번 확인해봐야겠어요
gh-pages로 배포할 때 꼭 package.json에 homepage 필드를 추가해야 하는 건 아니지만, 보통은 추가하는 게 좋습니다. 이유는 다음과 같아요:
1. homepage 필드 역할
React 같은 프레임워크에서 빌드할 때 빌드된 결과물 내에서 사용되는 경로(base URL)를 지정해줍니다.
예를 들어, GitHub Pages에서 보통 주소가 https://username.github.io/repo-name/ 형태인데,
기본 설정은 / (루트)로 인식해서 리소스 경로가 깨질 수 있어요.
homepage를 "https://username.github.io/repo-name" 또는 "/repo-name" 으로 지정하면, 빌드 결과 내에서 JS, CSS, 이미지 등의 경로가 정확히 맞춰집니다.
2. homepage 안 쓰면?
기본값이 /이기 때문에, index.html이 다른 경로에 있어도 리소스를 못 찾는 문제가 발생할 수 있습니다.
그래서 배포 후 화면이 깨지거나, 리소스 로딩 실패 오류가 나곤 해요.
3. 결론
GitHub Pages에서 프로젝트를 하위 경로(/repo-name)로 배포할 경우에는 homepage를 꼭 지정하는 것이 실수 방지에 좋습니다.
만약 도메인 최상위 루트(예: https://username.github.io/)로 배포한다면, homepage 필드를 생략해도 무방합니다.
| const hideAfter = useMemo(() => debounce(hide, DEFAULT_DELAY), [hide]); | ||
|
|
||
| const hideAfter = debounce(hide, DEFAULT_DELAY); | ||
| const showWithHide = useCallback<ShowToast>( |
There was a problem hiding this comment.
useAutoCallback을 쓰라는 힌트를 받아서 쓰긴했는데 useCallback으로도 가능하군요,,!!
| // 둘다 오브젝트 타입 아니면 값비교 | ||
| if (!isAObject && !isBObject) return Object.is(a, b); |
There was a problem hiding this comment.
console.log(Object.is("1", 1));
// Expected output: false
console.log(Object.is(NaN, NaN));
// Expected output: true
console.log(Object.is(-0, 0));
// Expected output: false
const obj = {};
console.log(Object.is(obj, {}));
// Expected output: false이런 기능도 있군요 배워갑니다.
| return renderedComponent; | ||
| } | ||
|
|
||
| return memoizedComponent.current; | ||
| }; | ||
|
|
||
| return renderMemoizedContent(props); |
There was a problem hiding this comment.
셋다 같은걸 반환하는데 각기 다르게 표현이 된 느낌이라 리턴을 줄이고 통일시켜도 좋을거같습니다!
ex
return renderMemoizedContent: FunctionComponent<P> = (props) => {
if (!propsRef.current || !equals(propsRef.current, props)) {
propsRef.current = props;
memoizedComponent.current = Component(props);
}
return memoizedComponent.current;
};
과제 체크포인트
배포 링크
bebusl.github.io/front_6th_chapter1-3
기본과제
equalities
hooks
High Order Components
심화 과제
hooks
context
과제 셀프회고
평상시에 쓰던 훅/hoc를 일부 구현해보면서, 그 훅들의 원리에 대해 좀 더 잘 알수 있어서 좋았습니다.
useAutoCallback은 실제 코드에서도 요긴하게 쓰일만한 훅이어서 가져다가 써보려고 합니다.
과제 해결 자체는 빨리 했는데 글쓰기(PR/블로그 등)를 회피해서 또 PR쓰기를 미루고 미루고 미뤄서 자괴감이 들었습니다. 남은 항해를 진행하면서 회고를 제때 남기는 버릇 + 그때그때 생각난 글감을 한 곳에 아카이브하는 습관을 만들도록 노력해야할 것 같습니다.
학습 효과 분석 및 기술적 성장
Fiber에 대한 학습
메모이제이션 관련된 hook/hoc에 대한 이해도 상승, 다른 커스텀 훅을 만들 때도 좀 더 정확히 이해하고 활용할 수 있을 것 같습니다.
useCallback(fn, [])처럼 deps 없이 고정시키면 오래된fn을 계속 참조해서 버그가 생길 수 있는데,useRef를 함께 쓰면 참조값은 유지하면서도 최신 함수 로직을 안전하게 반영할 수 있다는 깨달음을 얻었습니다.이 패턴은 특히 하위 컴포넌트에 콜백 props를 넘기거나, 외부 라이브러리 콜백 등록 시 유용할 것 같고 함수 재생성으로 인한 불필요한 렌더링 문제를 해결할 수 있어 실무에서 유용하게 사용할 것 같습니다.
pnpm을 이용한 모노레포 슬쩍 엿보기
자랑하고 싶은 코드
equals함수를 구현할 때 object형식인지 판별해야하는 곳이 있었는데, parameter자체는 unknown으로 정의되어 있었습니다.
이 부분을 typescript의 is 키워드를 이용해 좀 더 안전하게 타입 좁히기를 하면 좋겠다는 생각을 하였고, 아래와 같이 적용했습니다.
개선이 필요하다고 생각하는 코드
마찬가지로 equals함수 관련된 부분이었는데요,
shallowEquals, deepEquals 둘이 코드 베이스가 거의 똑같은데 그 부분을 따로 추출하여 쓰면 더 좋았을 것 같습니다.
(memo → deepMemo같은 구조로 갈 수 있었으면 좋았을 것 같습니다)
과제 피드백
hook/hoc 구현을 하는데 흐름이 자연스럽게 짜여있어서 이해하기에 좋았던 것 같습니다. 다음 hook은 어떻게 만들면 되겠구나, 저건 어떤 개념을 활용하면 되겠구나!가 자연스럽게 떠오르게 흐름이 잘 구성된 것 같습니다.
학습 갈무리
리액트의 렌더링이 어떻게 이루어지는지 정리해주세요.
메모이제이션에 대한 나의 생각을 적어주세요.
다른 곳에서도 성능 최적화를 위해 사용될 수 있겠지만 실시간성이 중요한 서비스, 예를 들어 라이브 방송처럼 여러 명의 유저가 동시에 상호작용하며 UI가 빈번하게 바뀌는 환경에서는 메모이제이션이 매우 효과적으로 활용될 수 있을 것 같습니다.
실시간 동영상 스트리밍과 같은 경우에 초 단위로 UI 갱신이 발생하므로 동일한 입력에 대해 중복 렌더링이나 연산을 피하는 것이 성능 유지에 중요합니다.
따라서 사용자 상호작용이 잦고 렌더링이 빈번히 발생하는 실시간 서비스에서 메모이제이션을 적극 활용하면 성능을 안정적으로 유지하는 데 큰 도움이 된다고 생각합니다.
컨텍스트와 상태관리에 대한 나의 생각을 적어주세요.
컨텍스트는 상태나 데이터를 관심사에 따라 분리해서 저장해두는 공간이라고 생각합니다.(예시: 테마 정보는
ThemeContext에, 사용자 정보는UserContext))컨텍스트를 이용하면 전역적인 정보를 한 곳에서 제공할 수 있어서 불필요한 props drilling을 막을 수 있습니다. 만약 컨텍스트를 사용하지 않고 일일이 단계별로 데이터를 내려준다면 추적이 매우 힘들어지고 코드도 복잡해질 것입니다.
상태관리는 UI에 영향을 주는 여러 사용자 상호작용이나 비동기 데이터 변화를 효과적으로 관리하는 작업이라고 생각합니다. 상태가 바뀔 때마다 UI가 원하는 대로 정확하게 반영되어야 하기 때문에 상태를 어디에, 어떻게 저장할지에 대해 정하는 것가 매우 중요하다고 생각합니다.
또한 상태관리는 단순히 값을 저장하는 것뿐만 아니라, 상태 변경의 흐름을 명확하게 만들고 예측 가능하게 만드는 과정이라는 점에서 개발자의 편의를 위한 중요한 작업입니다.
결국 우리가 직접 다루는 코드는 사람이 읽고 유지보수하는 것이기 때문에, 유지보수하기 쉽고 읽기 좋은 코드가 가장 중요하다는 생각입니다. 대부분의 기술이나 방법론은 바로 이 목표를 달성하기 위한 도구일 뿐이라고 느끼게 되었습니다.
리뷰 받고 싶은 내용
유틸 함수 분리 기준과 테스트 가능성 사이의 균형
유틸 함수를 작성할 때, 저는 150줄 미만 정도의 함수라면 굳이 세세하게 나누기보다는 기능 단위로 한 파일에 모아두는 방식을 선호하는 편입니다. 그런데 이번에
equals함수를 직접 구현해보면서, 타입에 따른 분기 처리를 한 함수 안에서 모두 처리했는숩니다.shallowEquals나 deepEquals 파일을 보시면 어레이 타입일때, 객체일때, 원시타입잍 때의 로직 모두 그냥 shallowEquals,deepEquals 안에 작성 되어 있는데 각 타입별 로직을 분리한 다음 equals함수에서 조립하는 식으로 분리하는 편이 더 좋았을 것 같기도 합니다.
근데 이런 조그마한 함수들이 너무 많아지면 오히려 관리가 더 힘들지 않을까하는 생각이 동시에 들기도 합니다.
코치님은 이런 경우, 어디까지를 "분리할 만한 단위"로 보시는지 의견이 궁금합니다.
명확함을 위해 긴 네이밍을 쓰는 게 좋을까요?
저는 변수나 함수 이름을 지을 때,
isEnabledByUserAction,fetchPostsByCategoryId처럼다소 길어지더라도 형용사나 전치사(by 등)를 붙여서 더 명확하게 표현하는 걸 선호하는 편입니다. 그런데 코드 리뷰를 하다 보면, 길다는 이유로 이름을 더 간결하게 바꾸자는 피드백을 받는 경우도 있어서 고민됩니다. 코치님은 협업 코드에서 가독성과 간결성 사이의 네이밍 밸런스를 어떻게 잡으시나요? 그리고 긴 이름이더라도 명확하면 괜찮다고 보는지, 혹은 더 좋은 네이밍 전략이 있는지 궁금합니다.