-
Notifications
You must be signed in to change notification settings - Fork 5
Refactor(client): 커뮤니티 queries 파일 mutationOptions 활용 리팩토링 #345
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
Conversation
|
✅ Storybook이 배포되었습니다. |
gwagjiug
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
내가 내 껄 읽어도 리뷰하기싫어지네.. 제송합니다 뚱뚱 PR 사죄드릴게요
| /** | ||
| * 특정 게시글의 모든 댓글을 페이지네이션으로 가져옵니다. | ||
| * @param postId - 댓글을 가져올 게시글 ID | ||
| * @param options - 페이지네이션 옵션 | ||
| * @param options.pageParam - 페이지 파라미터 (기본값: 0) | ||
| * @returns 댓글 응답 데이터 또는 null | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저희 해당 도메인에서 사용되는 모든 queryFn 과 옵션을 한 파일에서 관리하기 때문에, 추후 가독성 및 유지보수성을 고려해서 jsdocs 주석을 컨벤션으로 가져가고 싶어요 fn 단위에서만요! 다들 괜찮나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵 저는 좋습니다 ~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good 저도 원하던 바입니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 좋은 것 같습니다 ! 👍
hansoojeongsj
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
고생 많으셨어요!! PR 꼼꼼하게 잘 읽었어요.
tanstack query 관련 내용 정리해 주신 것도 그렇고, 공식 GitHub 토론까지 직접 찾아서 첨부해주신 점 정말 인상 깊었어요. 저도 번역기 열심히 돌려가면서 읽었습니다 ㅎㅎ
전반적으로 기존보다 코드가 훨씬 깔끔해졌고, options 분리 덕분에 관심사도 명확해져서 향후 유지보수나 확장성 측면에서도 훨씬 좋아질 것 같아요.
그리고 invalidateQueries가 반복되는 부분이 많아 처음엔 중복으로 느껴졌는데, 지욱님 PR에서 관심사 분리를 고려해서 호출부에서 처리하도록 설계하신 이유와 공식 문서 근거들을 보니 지금 구조가 확실히 더 유연하고 낫겠다는 생각이 들었어요!
한 가지 궁금한 점이 있는데, 글 수정 후에 커뮤니티 목록이 아닌, 해당 글 상세 페이지로 이동하도록 변경하신 건 혹시 원래 기획 의도였을까요? UX 측면에서 더 자연스럽다고 판단하신 걸까요? 선택하신 이유가 궁금합니다 :D
최고 웹리드 !!
| /* ======================================================= | ||
| * 📌 USER 관련 타입 | ||
| * ======================================================= */ | ||
|
|
||
| /** | ||
| * @description 내 프로필 정보 조회 응답 | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
주석 좋네요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
주석 처리하니까 훨씬 알아보기 편하네요 굿굿스
| /** | ||
| * 특정 게시글의 모든 댓글을 페이지네이션으로 가져옵니다. | ||
| * @param postId - 댓글을 가져올 게시글 ID | ||
| * @param options - 페이지네이션 옵션 | ||
| * @param options.pageParam - 페이지 파라미터 (기본값: 0) | ||
| * @returns 댓글 응답 데이터 또는 null | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵 저는 좋습니다 ~
| 'comment', | ||
| postId, | ||
| ], | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
COMMUNITY_QUERY_KEY에도 as const 붙여주실 수 있으신가요 ?!
| const queryClient = useQueryClient(); | ||
|
|
||
| if (!postId) { | ||
| throw new Error('postId가 없습니다.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사소하지만, 아래 Error처럼 '~가 존재하지 않습니다.' 로 통일해도 좋을 것 같아요 ㅎㅎ
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋네요 ㅎㅎ
이 부분은 앱잼 기간동안 기획쪽에서 원래 글 수정 후에 해당 글 상세 페이지로 이동해달라고 요청했으나 제가 추후에 수정해준다고 넘어갔었습니다 ㅎㅎ 그 부분을 이번 pr 에 반영했어요 |
제가 기획 상세에서 놓친 부분이 있었군요... 죄송합니다. 반영해주셔서 감사해요.. |
아니에요 구두로 논의한거라 정훈님이 몰랐던 사실인게 맞습니다 ㅎㅎ |
minjeoong
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
고생하셨습니다 !! ~~
| /** | ||
| * 특정 게시글의 모든 댓글을 페이지네이션으로 가져옵니다. | ||
| * @param postId - 댓글을 가져올 게시글 ID | ||
| * @param options - 페이지네이션 옵션 | ||
| * @param options.pageParam - 페이지 파라미터 (기본값: 0) | ||
| * @returns 댓글 응답 데이터 또는 null | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good 저도 원하던 바입니다!
| } | ||
|
|
||
| const { data } = useSuspenseQuery(POST_FEED_DETAIL_OPTIONS.DETAIL(postId)); | ||
| const { data } = useSuspenseQuery( | ||
| COMMUNITY_QUERY_OPTIONS.FEED_DETAIL(postId), | ||
| ); | ||
| const { | ||
| data: comments, | ||
| fetchNextPage, | ||
| hasNextPage, | ||
| isFetchingNextPage, | ||
| } = useInfiniteQuery({ | ||
| ...COMMUNITY_QUERY_OPTIONS.COMMENTS(postId), | ||
| }); | ||
|
|
||
| const { data: queryData } = useSuspenseQuery(USER_QUERY_OPTIONS.PROFILE()); | ||
| const { mutate } = useMutation({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
feed detail 데이터와 profile 데이터가 각각 data, queryData 라는 이름으로 사용되고 있는데,
두 데이터 모두 alias 를 지정해서 명확하게 분리하면 좋을 것 같아요!
ex)
const { data : feedDetailData } = useSuspenseQuery(
COMMUNITY_QUERY_OPTIONS.FEED_DETAIL(postId),
);
const { data: profileData } = useSuspenseQuery(USER_QUERY_OPTIONS.PROFILE());There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋습니다 기존 mutate 코드를 검색해서 vs code 상에서 switch 해버려서 이 부분을 놓쳤네용 다른 부분도 반영해놓을게요~ 밑에 리뷰도 같은 맥락인거죠?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵 ~! 감사합니다람쥐
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
생각해보니 이게 제 코드가 아니군요 수정은 해놓겠습니다 @jeonghoon11 님도 이 부분에 동의하신다면 추후 작업하실때에도 참고해주세요!
| } | ||
|
|
||
| const { data } = useSuspenseQuery(POST_FEED_DETAIL_OPTIONS.DETAIL(postId)); | ||
| const { data } = useSuspenseQuery( | ||
| COMMUNITY_QUERY_OPTIONS.FEED_DETAIL(postId), | ||
| ); | ||
| const { | ||
| data: comments, | ||
| fetchNextPage, | ||
| hasNextPage, | ||
| isFetchingNextPage, | ||
| } = useInfiniteQuery({ | ||
| ...COMMUNITY_QUERY_OPTIONS.COMMENTS(postId), | ||
| }); | ||
|
|
||
| const { data: queryData } = useSuspenseQuery(USER_QUERY_OPTIONS.PROFILE()); | ||
| const { mutate } = useMutation({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
지금 하나의 component 안에서 여러개의 mutate 를 쓰고 있어서 다른 mutate 들과 헷갈릴 우려가 있을 것 같아요.
해당 mutate 도 명시적으로 alias 를 주면 더 좋을 것 같습니다~!
| export const COMMUNITY_MUTATION_KEY = { | ||
| POST_COMMENT: () => [...COMMUNITY_QUERY_KEY.COMMENTS(), 'create'], | ||
| POST_FEED: () => [...COMMUNITY_QUERY_KEY.FEED_PREVIEW(), 'create'], | ||
| PUT_FEED: (postId: string) => [ | ||
| ...COMMUNITY_QUERY_KEY.FEED_DETAIL(postId), | ||
| 'update', | ||
| ], | ||
| DELETE_FEED: (postId: string) => [ | ||
| ...COMMUNITY_QUERY_KEY.FEED_DETAIL(postId), | ||
| 'delete', | ||
| ], | ||
| DELETE_COMMENT: (postId: string) => [ | ||
| ...COMMUNITY_QUERY_KEY.COMMENTS(postId), | ||
| 'delete', | ||
| ], | ||
| } as const; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mutation key 로 기존의 query_key 를 사용하는 건 처음인데,
좋은 것 같기도 하면서 만약 상황상 관련 query key 가 없다면 key 를 어떻게 줄 건지에 대한 의문도 들어요.
만약 어떤 도메인에서 query 를 쓸 일은 없고 mutate 만 쓰게 된다면 (QUERY KEY 가 존재하지 않는다면) 어떻게 MUTATION KEY 를 둘 건지에 대한 기준도 필요할 것 같은데 어떻게 생각하시나요?!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
POST -> 'create'
PUT -> 'update'
DELETE -> 'delete
... 이런 규칙을 가져가게 되는 건지 궁금합니다!
만약 그렇다면 쿼리 컨벤션에 추가기입해두면 좋을 것 같아요 ㅎㅎ
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
우선
mutation key 로 기존의 query_key 를 사용하는 건 처음인데,
부터 답변을 드리자면 정확하게는 둘이 다른 쿼리키를 사용하고 있어요 아마 mutation query key 객체 안에서 기존 query key 와 create 등을 조합해서 사용하고 있는데, 민정님이 언급하신 예외상황 (어떤 도메인에서 get 쿼리가 아예 없는 경우) 에서는 쿼리키 조합을 어떻게 할 것인가에 대한 기준을 말씀하신 것 같아요
사실 뮤테이션 쿼리키는 캐싱이 역할이 아니라 동일한 mutation 의 실행 여부를 관리하기 위한 id 값이기에 기존 queryKey 와는 완전히 다른 목적을 가지고 있어요.
그래서 기술적으로 두 개의 쿼리키가 완전히 동일한 배열이라도 동작에는 문제가 없어요 하지만 의도가 다르기에 헷갈리지 않기 위해 뮤테이션 쿼리키를 별도로 선언해두었어요
하지만 민정님이 말씀하신 예외상황에서는 그냥 ['도메인', '액션'] 과 같이 고유하게 선언하면 될 것 같아요. 결국 중요한건 mutationKey는 동일 mutation 을 그룹핑 하는 용도라는 점이고 고유성과 의도만 보장된다면 기존 쿼리키 + 액션을 조합한 방식 또한 사용하지 않아도 된다고 생각합니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
두 번째 답변은 일단 임의로 그런 규칙으로 쿼리키를 조합해둔 것 맞습니다! 괜찮다면 컨벤션에 등록해두도록 하겠습니다 ㅎㅎ
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넹 의견 감사합니다!
부터 답변을 드리자면 정확하게는 둘이 다른 쿼리키를 사용하고 있어요 아마 mutation query key 객체 안에서 기존 query key 와 create 등을 조합해서 사용하고 있는데, 민정님이 언급하신 예외상황 (어떤 도메인에서 get 쿼리가 아예 없는 경우) 에서는 쿼리키 조합을 어떻게 할 것인가에 대한 기준을 말씀하신 것 같아요
넵 맞아요 키는 다르고 query key 를 이용해서 Muation key 를 만드는 경험에 대한 관점이였습니다!
예외상황에서의 키 조합에 대한 기준을 정해두면 좋겠다고 생각했어요 ㅎㅎ
지욱님 말씀처럼 만약 매칭되는 쿼리 키가 없는 상황에서 ['도메인', '액션'] 으로 정해둔다면 너무 좋은 것 같습니다~!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
만약 해당 예외 경우가 발생한다면 발생한 작업 PR 에서 얘기해보고 컨벤션을 정해보는 것도 좋아보이네요 ㅎㅎ
jeonghoon11
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
앱잼 때 커뮤니티 쿼리를 구현할 당시에는 Tanstack Query에 대한 이해도가 부족해서 사실상 구현 위주로만 작업을 진행했던 터라 전체적으로 스파게티 코드가 되어버린 부분이 많았는데 이번에 그걸 깔끔하게 구조화해서 리팩토링해주셔서 정말 감사드립니다 🙇
특히 mutationOptions를 따로 분리해서 관심사 분리를 해주신 점, mutationKey도 의도에 맞게 따로 정의하신 점 type 주석을 달아 가독성까지 챙겨주신 부분 이런 디테일한 부분까지 신경 써주셔서 보고 많이 배웠습니다.
덕분에 커뮤니티 쿼리 전반의 스타일과 방향성이 정리된 느낌입니다. 정말 수고 많으셨습니다 💯
| /* ======================================================= | ||
| * 📌 USER 관련 타입 | ||
| * ======================================================= */ | ||
|
|
||
| /** | ||
| * @description 내 프로필 정보 조회 응답 | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
주석 처리하니까 훨씬 알아보기 편하네요 굿굿스
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
고봉밥 PR을 읽으면서 tanstack query를 사용하면서 생겼던 궁금증들이랑 몰랐던 부분들 많이 배워갑니다.
특히 mutationOptions를 사용하는 이유를 보면서 queryOptions, mutationOptions 등 옵션에 대해서도 더 알아가는 것 같아요.
pr을 읽으면서 느끼는건데 지욱님의 코드에 대한 고민과 코드에 대한 근거를 열심히 찾으시는 모습이 정말 감동입니다..
그리고 전체적으로 community 관련 쿼리 코드가 훨씬 깔끔해지고 코드 순서나 구조도 정돈되어 있어서 읽는 데 정말 정말 편했습니다.
수고하셨어요 !! 👍
| /** | ||
| * 특정 게시글의 모든 댓글을 페이지네이션으로 가져옵니다. | ||
| * @param postId - 댓글을 가져올 게시글 ID | ||
| * @param options - 페이지네이션 옵션 | ||
| * @param options.pageParam - 페이지 파라미터 (기본값: 0) | ||
| * @returns 댓글 응답 데이터 또는 null | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 좋은 것 같습니다 ! 👍
| }, | ||
| COMMENT: { | ||
| title: '이 댓글을 삭제할까요?', | ||
| content: '삭제한 댓글은 복원되지 않습니다.', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DELETE_MODAL가 import 문 중간에 존재하는데 밑으로 내려주실 있을까요...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제 코드가 아니긴한데.. 저도 인지하고 있던 부분이에요 고치지 않은 이유는 커뮤니티 관련 코드 리팩토링을 @jeonghoon11 님에게 맡기고 싶어서에요
현재는 로직 자체가 너무 복잡하기도하고.. 코드의 전개 순서도 많이 복잡하다고 생각해서 다음 작업에서 정훈님이 개선해주시면 좋을 것 같아요
p.s 모달을 렌더하는 코드는 아예 별도로 분리하는게 좋을 것 같습니다
| queryClient.invalidateQueries({ | ||
| queryKey: COMMUNITY_QUERY_KEY.COMMENTS(variables.postId), | ||
| }); | ||
| queryClient.invalidateQueries({ | ||
| queryKey: COMMUNITY_QUERY_KEY.FEED_DETAIL(variables.postId), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저는 그동안 사용하는 컴포넌트는 무조건 깔끔해야 한다고만 생각했어요. 그런데 지욱님이 invalidateQueries를 실제 사용하는 컴포넌트에서 호출해야 하는 이유를 근거를 들어 설명해주시고 사용하는 쪽과 사용되는 쪽의 코드를 각각 깔끔하게 유지하면서도
mutation의 실행 결과가 성공했을 때만 invalidateQueries를 호출하는 것이 적절하다는 관점으로 관심사를 분리하신 게 너무너무 인상깊네요... 지욱님의 방법이 완벽한 정답이 아니더라도 너무 좋은 것 같아요 .. 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 사실은 이번 작업을 하기전에는 onSuccess 와 같은 사이드 이펙트를 중앙에서 관리하고 있었던 것 같아요. 하지만 mutationOption는 그 자체로 옵션만 정의하는 역할만 수행해야 인터페이스 자체의 설계철학을 100% 활용하는 방법이라고 생각해요 onSuccess 시에 어떤 작업을 추가로 실행할지는 option 을 사용하는 사용처에 맥락에 따라 달라질 수 있기 때문에 지금의 방식이 맞다고 생각합니다 ㅎㅎ
📌 Summary
📚 Tasks
👀 To Reviewer
커뮤니티 페이지를 담당하는 queries 파일을 리팩토링 했습니다. 다른 페이지에서는 get 요청밖에 존재하지 않기도하고, 정형화 된 코드를 제시하고 싶어서 작업했어요.
우선 첫 번째로 기존의 types 파일안에 존재하는 api 응답 타입 선언부에 주석을 추가했습니다. @description 태그로 간단하게 용도를 정리하였고 개인적으로는 가독성이 훨씬 좋아졌다고 생각해요.
두 번째로 무한 스크롤 관련 로직을 수정하였어요. 기존에는
getNextPageParam을useInfiniteQuery호출 시점에 직접 주입하고 있었는데,(내가 주장함) 저희 프로젝트의 경우getNextPageParam로직이 전혀 변하지 않고, 호출 시점마다 다른 페이징 전략이 필요한 부분이 아니기에, 선언부에getNextPageParam로직을 중앙화 하는 것이 더 바람직하다고 판단했습니다. 특히 api 호출부 쪽 로직이 너무 길고 복잡하여 호출부에서getNextPageParam로직을 주입할 경우 코드 복잡도가 매우 상승하더라구요..해당 부분 외에도
infiniteQueryOptions를 추가적으로 사용하였는데, 이 api 를 사용하는 것에 장점은 다음과 같아요.getNextPageParam,initialPageParam와 같은 필수 옵션이 누락되면서 발생할 수 있는 타입 에러를 방지할 수 있어요UseInfiniteQueryOptions을 기준으로 강제되어요queryKey타입도 자동으로readonly unknown[]으로 강제되어요lastPage타입도 명확해지고 중간에 휴먼에러를 방지할 수 있어요세 번째로 tanstack Query v 5.82.0 부터 추가 된 mutationOptions 을 활용하여 mutation 로직을 개선하였어요. mutationOptions 는 useMutation, useIsMutating, queryClient.isMutating 등 다양한 인터페이스에서 재사용될 수 있어요
mutationOptions 는 사실 엄청 큰 기능은 아니라고 생각해요. 단순히 tanstackQuery 의 queryOption 와 비슷한 맥락에서 만들어진 helper 함수 정도라고 보시면 될 것 같아요.
mutationOptions가 만들어진 이유는 기본적으로 useMutation 의 인자로 넣은 옵션 객체를 미리 선언해두고 공통화/재사용 하자는 용도라고 생각해요 그런 의미에서 거의 queryOption 과 대칭적인 의미를 가지고 있어요 앱잼 때 설명을 이어가보자면 queryOptions 는 useQuery , useInfiniteQuery 에서 동일한 쿼리 옵션을 공유할 때 사용할 수 있듯이 mutationOptions 도 같은 철학으로 여러 useMutation 이나queryClient.isMutating를 같은 곳에서 같은 키와 함수를 재사용하려고 만든 것인 것 같아요여기까지 듣다보면 사실 아직은 쿼리를 재사용 할 정도로 프로젝트 규모가 크지 않고 미래 계획도 불분명한데 왜 도입한거지? 라는 생각이 들 것 같아요.
저는 이 부분 초기에 확장성을 갖춘 설계를 해놓는 것 외에도 장점이 있다고 생각해요
프로젝트 규모와 상관 없이 코드 스타일 일관성 유지가 가능해요. queryOptions 를 이미 쓰고 있다면 mutationOptions 도 같은 패턴으로 사용할 경우 패턴으 통일되어요. 당연히 패턴이 통일 되면 추후 스프린트에서도 생산성이 증가한다고 생각하구요. 즉, 실질적인 이득이 아니라 코드가 정형화되고 한 눈에 규칙이 보인다는 점이 이 인터페이스 도입의 가치라고 생각해요
그리고 추후에
useIsMutating을 쓸 가능성이 조금이라도 있다면,mutationOptions를 강제하여 mutationKey 를 선언하는 것을 강제할 수 있어요.추후에 mutaion 이 늘어날 경우 지금부터 mutationFn, mutationKey 를 묶어서 한 곳에서 관리하면 초기 설계에서 완전한 유연함을 가져갈 수 있을 거라고 판단했어요
이 제가 해당 인터페이스를 도입한 본질이에요 당장 유틸로서 크게 유용한 기능을 기대하지는 않아도 되지만 디스코드에 첨부해드린 문서를 한번 읽어보시면 도움이 많이 될 것 같아요.
복잡한 options 내의 로직을 어떻게 할까?
이게 제가 리팩토링을 하던 중간에 저장해놓은 mutationOptions 의 초기 형태에요 😓 감각적으로 봐도 너무 내부 로직이 복잡하고 관심사가 너무 얽혀있는 모습이에요
사실 mutationOption 는 옵션만 정의하는 역할이어야 깔끔한데, 리액트 훅까지 호출하고, 쿼리 키 초기화와 같은 사이드 이펙트(캐시 무효화) 로직또한 중앙 옵션에서 정의하는 것에 대해서 dna 가 거부감을 느끼고 있었어요..
이 부분 유지보수성을 높이기 위해서
mutationOptions는 mutationKey + mutationFn 까지만 갖고,
onSuccess는 실제 useMutation 호출부에서 정의, invalidateQueries도 호출부에서 처리하도록 변경하였어요.
지금과 같은 모습으로요.
mutationOptions 는 중앙에서 순수 정의만해서 관심사가 깔끔해졌고,
invalidateQueries는 실제 사용하는 화면 혹은 컴포넌트에서의 요구사항에 맞게 조절할 수 있게하여 재사용이 완전히 free 할 수 있도록 설계했어요.이 부분 근거를 찾기 위해 공식문서를 뒤져보았어요..
ref : https://tanstack.com/query/v4/docs/framework/react/guides/query-invalidation
제가 이해한게 맞는지는 확실하지 않으나 공식문서에서는 다음과 같이 언급하고 있어요.
when?확실히알 때내가알고 있을 때why?더 간단하게 설명하면 "서버에 데이터를 쓰거나 지운 뒤 관련된 쿼리를 무효화한다" 라는 의미를 갖고 있어요.
즉 mutation의 실행 결과가 성공했을 때만 실행되어야 하고, mutationOptions 선언부에서 invalidateQueries 를 바로 호출 할 경우 선언 시점은 훅이 아니기 때문에 queryClient 를 안전하게 사용할 수 없다고 얘기하고 있어요. 관심사 분리가 깨지는 것도 이유고요.
useEffect 등에서 invalidateQueries 를 하면 안된다고 얘기하는 것도 같은 맥락이구요.
mutation 쿼리키 왜 필요함?
저도 이 부분 고민이 많아서 실제로 mutationOptions 의 도입이 논의된 PR에서 tanstackQuery 팀의 토론을 구경해보았어요..
ref: TanStack/query#8960
한번 읽어보시길 권장드리고 결론부터 말씀드리면, mutationKey 동일한 mutation 그룹을 식별하는 ID 역할을 하고, 이 ID가 있어야 queryClient.isMutating, useIsMutating 등에서 정확히 무슨 mutation이 실행 중인지 알 수 있기 때문에
mutationKey를 강제해야 한다고 얘기하고 있어요.queryOptions가 queryKey 를 필수로 요구하는 것과 같은 맥락으로요.여기서 또 고민이 된 점은 mutation의 mutationKey는 Query의 queryKey랑 같아도 되지 않나? 라는 고민이었어요.. 이 부분도 알아봤는데
같아도 문제는 없어요. 둘은 기능적으로 다른 역할을 하기 때문에 내부적으로는 단순히 배열로 키를 맞춰서 비교할 뿐이기 때문에 기술적으로 동일한 배열이라도 문제는 없어요.
하지만
queryKey는 데이터 캐시의 식별자 역할을 하고, mutationKey 는 동일한 mutation 실행 여부를 필터링 할 때의 식별자 역할을 하기에, mutaion 에서의 쿼리키는 의도가 달라야 한다고 생각했어요.그래서
이렇게 mutaion_key를 새롭게 선언해두었습니다.
기술적으로 같아도 되지만 api 안정성, 디버깅,가독성에서도 손해를 본다고 생각했구요 mutaionOptions 도 그 의도가 완전히 다르기에 key 또한 의도가 완전히 다르게 드러나는 이름으로 짓고 싶었습니다.
📸 Screenshot
이 부분 원래는 수정이후에 커뮤니티 전체 페이지로 넘어갔지만, 수정 완료 이후에 해당 글로 navigate 되도록 수정했습니다.
before
2025-07-24.1.03.46.mov
after
2025-07-24.1.04.03.mov