Conversation
🚀 CI Check Results
|
🚀 CI Check Results
|
rtttr1
left a comment
There was a problem hiding this comment.
복잡한 로직 구현하시느라 고생 많으셨습니다!
항상 상세한 Pr 써주셔서 이해하는데 많은 도움이 됩니다 너무 감사합니다~~
| ); | ||
| const { mutate: commentEditMutate } = usePatchNoticeCommentMutation(); | ||
|
|
||
| const handleCommentEdit = useCallback( |
There was a problem hiding this comment.
여기서 useCallback을 사용하신 이유가 무엇일까요?
어차피 input에 타이핑해서 comment 상태가 변하면 CommentBox까지 리랜더링이 진행될텐데 useCallback으로 감싸서 넘겨주는 이유가 궁금합니다!
There was a problem hiding this comment.
isEditing 값을 atom을 통해 바로 가져오는 함수를 만들었다가 무한 루프를 만났는데, useMutation 관련 문제로 착각해서 이리저리 만지작댄 흔적이에요.
export const isItemEditingAtom = (itemId: number) => atom((get) => get(editingItemIdAtom) === itemId);위 코드가 바로 댓글 id를 인자로 받아서 isEditing값을 바로 얻어내는 atom인데, 객체 자체를 저장하는게 아니라 함수를 반환하고 있어요. 그 결과 이걸 사용하면 매 렌더링마다 atom 객체가 반환되고, 새 객체로 바뀌었으니까 리렌더링이 되는 무한루프에 빠진거였죠.
useCallback을 쓰게 된 이유는 이렇고, 이 부분을 성능 측면에서 다시 보면 handleCommentEdit외에 deleteMutate도 감싸야 하고, comments도 처리하고 어쩌구저쩌구 하는것 보다는 아예 댓글 다는 컴포넌트를 따로 빼서 setState의 영향을 좁히는 방향으로 가는게 좋을거에요. 결국 처음 컴포넌트를 만들때 하나에 다 몰아넣어서 만든 탓에 생긴 기술부채인거죠.
| // form, textarea는 isEditing 상태에 따라 조건부로 렌더링되기 때문에 사용됨 | ||
| flushSync(() => { | ||
| setCurrentEditItem(commentId); | ||
| }); |
There was a problem hiding this comment.
오 배칭 때문에 타이밍이 꼬일때 이렇게 해결하는 방법이 있군요!
There was a problem hiding this comment.
제가 짠 코드는 아니지만 보기 드문 함수인 flushSync가 쓰인 이유를 써 놓는게 좋겠다 싶어 붙인 주석이에요
✅ Done Task
☀️ New-insight
노션, 리니어 같은 현대의 앱들이 얼마나 사용하기 편하게 만들어져 있고(이제 algohub도 명함 내밀기 가능), 그걸 위한 구현 복잡도가 얼마나 어려운 지 알게 됐어요. 그리고 항상 클린 코드를 유지해야 이런 복잡한 구현도 쉽게 할 수 있다는 것을 다시 한번 깨달았네요.
💎 PR Point
CommentBox리팩토링기존
CommentBox컴포넌트는 두 곳의 사용처(공지, 내 풀이)를 위해서 만들어진 일종의 강결합 컴포넌트입니다.mutation 함수가 요소 내부에서 정의되고,
useEditForm훅이 반환하는control[variant]를 통해 사용처에 해당하는 값과 핸들러 함수들을 사용하는 식이었습니다. 또한 수정 활성화된 댓글의 id를 상태로 저장하기 위해 context api를 필수로 사용해야 하는 구조이므로 여기저기 신경 써야할 곳이 많습니다.그 결과
공지용으로CommentBox를 사용할 땐내 풀이용 상태와 함수들은 사용하지 않으며(반대도 마찬가지) 구조를 파악할 때도 엇비슷한 기능의 두 종속성이 겹쳐서 코드를 읽거나 수정하기 어려운 문제가 있었습니다.이를 해결하기 위해 두 사용처의 엇비슷한 기능(사실 동일한데 굳이 분리해놓은...)을 합쳐 일반화하고 다른 사용처를 추가하기도 편하게 mutation을 부모로부터 받아오도록 수정하였습니다.
상호작용 방식 개선
어떤 문제가 있었는지는 Linear 이슈를 참고해주세요!
댓글 내용이 변경되지 않아도 수정 완료 시 api를 호출하던 것을 방지하고 아래 개선 사항들의 원활한 적용을 위해
react-hook-form의isDirty필드를 사용하여 검사합니다.수정 활성화 시 우상단 버튼의 기능 전환 (수정/삭제 → 완료/취소)
handleEditBtnClick핸들러에서isEditing값에 따라 수정 활성화를 할 지 수정 완료를 할 지 분기를 추가했습니다.삭제/취소의 경우도
isEditing에 따라 조건적으로 작동하며, 수정이 아니니까useEditComment훅에서 할 필요는 없다고 생각하여CommentBox에서 바로 핸들러를 정의했습니다.수정 중에 다른 댓글 수정 활성화 시 수정 완료 처리
isEditing파생값을 만들기 위해 수정 활성화된 댓글의 id를 저장해야 합니다. 이를 위해 사용했던 context api지만, 보일러 플레이트가 많이 필요하여 사용하기 불편하므로 jotai(atom)로 교체했습니다. 기존context api의setState와clearState는store.ts로 옮겨졌고, 컴포넌트에서는 이 값과 함수들을 가져다 사용합니다.이 기능의 구현을 위해
useEffect와usePrevious로 이전 id값을 검사하여if (prevEditingItemId === commentId && !isCurrentEditing)이면 수정 완료를 하도록 했습니다.수정 중에 바깥 요소(모달 닫기 포함) 클릭 시 수정 완료 처리
useOutsideClick훅을CommentBox에 적용하여 구현했습니다. 바깥 영역 클릭 시handleEdit함수를 호출하여 자동으로 저장합니다. 모달을 닫을 때도 자동 저장을 해주기 위해useOutsideClick의 콜백 호출 시점을mousedown이 아닌click으로 변경하였습니다. 호출 시점을mousedown으로 하면 다른 댓글은 현재 댓글의 바깥이므로 콜백인handleEdit의clearItemId가 호출되어 수정 기능 활성화가 안되기 때문입니다. (둘의 호출 시점을 다르게 적용하면 모달이 먼저 언마운트돼서 저장이 안됩니다)낙관적 업데이트
다음 기준(로직)으로 적용했습니다.
📸 Screenshot