DYO 공부하는 블로그
[데브코스 프론트] PickItBook 프로젝트 회고 본문

배포 주소 https://pick-it-book.vercel.app/
PickitBook
pick-it-book.vercel.app
프로젝트 레포지토리 https://github.com/prgrms-fe-devcourse/FES-5-Project-TEAM-6
GitHub - prgrms-fe-devcourse/FES-5-Project-TEAM-6: 룰렛 기반 책 선택과 도전과제와 같은 미션으로 책에 대한
룰렛 기반 책 선택과 도전과제와 같은 미션으로 책에 대한 관심을 높이는 PickItBook 프로젝트입니다. - prgrms-fe-devcourse/FES-5-Project-TEAM-6
github.com
1. 프로젝트 개요
- 프로젝트명: PickItBook
- 기간: 2025.08.22 ~ 2025.09.07
- 목표:
- 책 선택의 어려움 해결
- 독서 습관 형성을 위한 게임화 요소 도입
- 다양한 장르와 미션을 통한 새로운 독서 경험 제공
와이어프레임

프로젝트 아키텍쳐 구조

핵심 기능
- 룰렛 UI
- 책 표지가 슬롯처럼 돌아가는 애니메이션(Framer Motion, Lottie 등 활용)
- 장르/인기작/연령/성별 등 필터로 후보 범위 설정
- 필터 적용된 후보 중 랜덤 도서 추첨
- 룰렛 결과 도서 정보, 미션 제시
- 룰렛 결과 도서 리뷰 시 점수/토큰 획득
- 책 데이터 소스 및 API
- Google Books API로 표지・메타데이터 자동 불러오기
- 국내 도서관 소장 여부: 국립중앙도서관 OpenAPI, 도서관 정보나루 API
- ISBN 기반 데이터 통합
- 전자책 보유여부: 각 도서관/사이버도서관 API 확인
- 필터/검색
- 장르, 연령대별 선호도, 성별
- 키워드 기반 검색, 제목 검색, 작가명 검색
- 랜덤 독서 챌린지
- 룰렛 결과와 함께 난이도별 미션(예: 리뷰 작성하기, 3줄 요약하기)을 랜덤으로 제시
- 미션 완료/진행 상태 트래킹, 뱃지 획득
- 개인 서재 & 업적
- 서재에 읽은 책 추가, 통계・데이터 보여줌
- 대시보드에 업적/진행상황 보여줌
- 통계・데이터 시각화
- 월간 독서량/미션 추이/장르 편중(Rechart, Chart.js 등)
- 독서 성향, 인기 도서 통계 시각화
- 리뷰/3줄요약
- 리뷰/별점/3줄 요약 등록(이미지, 텍스트) → 책 정보 페이지에 보여줌
2. 프로젝트 팀 플래그
1. 새로운 기술 적용해보기
기존에는 바닐라로 기능을 구현해보는 데 집중했다면, 자주 사용되는 라이브러리를 공부하고 적용해보기로 결정했다.
2. 라이브러리 사용에는 이유가 있어야 한다.
Zustand 많이 쓰니까. Tanstack Query 많이 쓰니까. 같은 이유로 기술을 선정하는 게 아니라 Zustand의 상태 관리, 트리거 로직파악하고 적용했을 때 무슨 장점이 있는지, 비슷한 라이브러리 사이에서 왜 이것을 선택해야 하는지를 팀 단위 회의로 적용하기로 했다.
3. 컴포넌트 재사용성
재사용될 가능성이 있는 컴포넌트를 주기적으로 공유하고, 반복적인 코드 작성을 줄이며 생산성을 높인다.
위와 같은 팀 플래그를 팀 단위로 정했다.
이번 프로젝트가 새로운 기술을 배울 수 있으며, 생산성을 높이는 활동에 익숙해지는 것을 이번 프로젝트의 플래그로 잡았다.
3. 프로젝트 사전준비
1. 기술선정
Tanstack Query
외부 API를 사용하기 때문에 staleTime을 이용한 요청량 제어가 유효하다고 판단했다. 옵션을 사용해 쿼리 제어가 간편하고 요청 상태에 따른 화면 제어, 성공, 실패 동작을 구분해 폴백과 데이터 갱신에 강점이 있어 Tanstack Query를 선정하게 되었다.
GSAP vs Framer
애니메이션을 관리하는 데 있어 Framer와 Gsap 사이에서 어떤 것을 사용할지 고민했는데, framer는 기본적인 애니메이션 구현시 쉽게 사용할 수 있고, 리액트 친화적이라 선언형으로 다룰 수 있는 장점이 있고, Gsap은 명령형이었기 때문에 사용해봤던 라이브러리인 Gsap을 사용할지, Framer를 적용할지 고민했었다.
팀 내부에서 회의한 결과, 스크롤 제어 애니메이션에 Gsap이 장점이 있고, 상대적으로 익숙했기 때문에 Gsap을 선택하게 되었다.
Three.js
북마크된 책을 쌓아서 내 서재에 책을 얼마나 담았는지 확인하는 기능을 구상했는데, 북마크된 책이 많아짐에 따라 2d에서는 보여줄 수 있는 한계가 존재했다. 그래서 자유로운 시점 변경으로 쌓인 책을 확인할 수 있도록 하기 위해 Three.js를 선택했다.
husky
커밋 전 코드 스타일을 통일화해 코드 충돌 방지 및 코드 일관성을 유지할 수 있도록 하기 위해 선택했다.
zustand
전역 상태 관리를 위해 선택하게 되었다. 그러나 전역 상태의 위험성이 있어 유저 정보, 루트 메뉴 제어와 같은 공통적인 부분만 스토어로 관리하고 일반적인 fetching cache는 Tanstack Query를 이용해 관리하기로 했다.
2. 기능 명세화 / 요구사항 정의
기획서에 따라 프로젝트의 기능을 명세화하고 요구사항을 정의했다. MVP로 가장 먼저 구현해야 할 기능들을 정의하고 진행했다.
https://docs.google.com/spreadsheets/d/1GLTCr1VRuY3dfdQKeFi2NBCkYMvbyxDzWoFEoVdej9Y/edit?gid=0#gid=0
PickItBook 요구사항
ABCDEFGHIJKLMNOPQRSTUVWXY카테고리요구사항설명릴리즈룰렛필터 적용 후보 도서 추출장르 / 분량 / 인기 / 연령 / 성별 / 읽음제외 적용MUSTMVP룰렛스핀 애니메이션2–4초 가속-감속, 마지막 300ms 하이라이
docs.google.com
3. 데이터베이스 구조
초기 구조는 3NF 기반으로 설계했는데, 프로젝트를 진행하면서 조금 불안정해진 면이 있다. ( 잘 관리했어야 했는데 아쉬운 부분이다.)
데이터베이스는 supabase로 관리했고, 조회는 View 또는 RPC 삽입은 테이블 직접 삽입이라는 규칙을 정해 데이터베이스 규칙을 관리했다.
4. 내가 구현한 파트
1. 시연 영상, 기여 목록
가장 오래 작업한 파트 시연 영상만 가져와봤다.
동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.
이 페이지의 Swiper, 미션 모달 제외한 파트 전체를 작업했다. 미션 트리거는 트리거 작업과 뒷배경에 애니메이션 까지만 가져오고 모달 자체는 팀원이 작업했다.
전체 프로젝트 기여 목록은 다음과 같다.
- 프로젝트 진행 관리
- 프로젝트 초기 설정
- 초기 데이터베이스 생성
- Root 컴포넌트
- 초기 라우팅 구조 생성
- ScrollTopButton 초기 디자인, 기능 연결
- Search Page
- 반응형 구현
- SearchList 관련 컴포넌트 작성
- 검색 기능 추가
- 검색 결과 리스트 ‘Grid’, ‘Line’ mode 변경 구현
- Tanstack Query를 이용한 useBookFetching 작성
- 페이지 네비게이션 컴포넌트 작성
- Filter 컴포넌트 구현, Gsap 애니메이션 추가
- Detail Page
- MissionPartition 컴포넌트 작성 - 미션 목록
- BookDataPratition 컴포넌트 작성 - 책 상세정보
- UserScorePartition 컴포넌트 작성 - 리뷰 점수 종합
- ReviewWritePartition 컴포넌트 작성 - 리뷰 작성
- ReviewListPartition 컴포넌트 작성 - 리뷰 목록
- 좋아요 상호작용
- 댓글 기능
- Fetching, Pending 상태를 이용한 Load 애니메이션으로 UX 개선
- 모든 데이터 QueryKey 무효화를 이용해 Stale Time 초기화해 서버의 변경사항 즉시 반영되도록 작성
- 좋아요, 미션 수령에 대해 낙관적 업데이트 적용 ( 실패 시 Fallback )
- 재사용성 고려, Partition으로 각 섹션을 분리, 재사용될 Bookmark, RatingStar 등의 컴포넌트 유연하게 구현
- Mission 트리거, 이벤트 관리
- 미션 관리 테이블
- task_templates(미션 데이터)
- task_bundle(미션 묶음)
- task_bundle_items(묶음 아이템)
- user_tasks(유저가 수령한 미션)
- user_task_event_log(유저 활동 이벤트 로그
- user_book_task_assignment(유저가 수령한 번들)
- task_reward(수령한 보상 목록) 작성
- 이벤트 트리거 로직 작성 및 연결
- api_assign_book_tasks- 결정적 난수로 미션 목록을 뽑아 유저에게 부여합니다. (task, bundle 등 처리)
- api_process_event - 유저의 행동에 대한 이벤트 로그를 부여하고 트리거 로직에 따라 보상 여부를 판별합니다. ( 리뷰 작성, 북마크 추가 등 )
- getBundleIdByISBN - isbn에 대한 미션 번들 번호를 받아옵니다. 각각의 이벤트 ( 리뷰 작성, 북마크 추가 ) 등에 대해 이벤트 로직 추가 또는 트리거 부여
- realtime 구독 추가 task_reward에 대해 Realtime 로직을 추가하고 로그인한 유저가 미션을 완료했을 경우 콜백을 실행하도록 설정
- 미션 관리 테이블
- AWS EC2 이용한 Express + Nginx 프록시 서버 구현
- White List 관리를 위해 Vercel로 중계하는 프록시 서버 구현
- 로컬 호출도 Proxy 호출하도록 변경
그전부터 DB 관련 작업, 백엔드 관련 작업을 많이 작업했어서 프론트엔드 학습하는 과정에서 좀 벗어나지 않나... 하는 고민을 했는데, 프로젝트 자체가 불안정한 것이 너무 마음에 안 들어서 이번에도 백엔드쪽 작업을 섞어서 작업하게 되었다.
2. 검색 기능 구현

장서 데이터는 외부 API를 이용해 구현했다. 단순하게 외부 API에 요청해 데이터를 화면에 렌더링하기만 하면 됐다.
그런데 response로 들어오는 데이터도 생소해 타입 정의하기도 어려웠고, Tanstack Query를 처음으로 적용한 기능이라 조금 헤맸다. 결과적으로 데이터도 잘 받아왔고, Tanstack Query의 useQuery도 성공적으로 적용해 staleTime과 refetchOnWindowFocus와 같은 옵션들을 이해하고 사용할 수 있게 되었다.
이 페이지를 구현하면서 제일 신경 쓴 부분은 아주 단순하게 펼쳐지는 필터 드롭다운이었다.

필터 컴포넌트가 재사용될 수 있으면서, 서브메뉴가 있든 없든 작동하고, 조금 더 인터렉티브하게 만들고 싶었다.

위 gif에서 나오는 필터 컴포넌트와 같은 컴포넌트이다. 컴포넌트의 아이템은 대분류와 소분류 아이템으로 나뉘는데, 0부터 10 단위로 0, 10, 20 ...은 대분류이고, 1, 11, 12와 같은 아이템은 10단위로 끊긴 대분류의 내부 아이템들이다. 대분류만 필요할 경우 10단위로 끊긴 아이템들만 사용하면 서브 아이템은 표시되지 않고, 서브 아이템에 대해서 gsap으로 인터렉티브하게 구현했다.
클릭 이벤트가 발생하면 상위 요소에서 아래와 같은 데이터를 받을 수 있다.
// 상위 컴포넌트에서 받을 수 있는 데이터
{top:{code:'20', value:'사회과학'}, bottom:{code:'21', value:'통계학'}}
결과적으로 성공적으로 다른 팀원의 페이지에 정착한 공유 컴포넌트를 만들 수 있었다.
3. 상세 페이지

상세 페이지는 책 정보들을 외부 API로부터 불러와 책 정보를 보여주도록 했다. 관련 미션, 북마크, 평점, 리뷰와 같은 유저의 활동에 의한 데이터는 supabase에서 관리했다.
이 페이지를 구현하면서 가장 신경 쓴 부분은 두 가지다.
첫째로, 상위 컴포넌트를 dumb하게 유지하고 하위 컴포넌트에서 mutation과 조건 처리와 같은 로직을 수행하도록 하는 것이다. Container/Presentational Pattern을 이용해보고 싶었다.

최상단 Page 컴포넌트는 단순히 Query만 수행하고 하위 요소에서 Mutation, 조건부 처리와 같은 로직을 수행하는 구조를 만들어 각각의 컴포넌트에 대해 따로 테스트를 수행할 수도 있으며, 재사용 될 수 있도록 만들었다.
두 번째로, 각각의 동작에 대해 데이터가 연동성을 가질 수 있게 하는 것이었다. 리뷰를 작성했다면 그 페이지에서 즉시 리뷰가 갱신되어야 하고, 미션을 완료했다면 그 즉시 미션 목록에서 미션 완료로 갱신되어야 했다.

Tanstack Query의 useQueryClient를 이용해 리뷰를 작성할 경우 목록에 즉시 추가되고, 리뷰 관련 query들의 staleTime을 즉시 무효화시키도록 invalidQuerys를 이용해 바로 갱신되도록 신경썼다.
리팩터링 시 개선 사항
1. 각 섹션에 대해 Partition으로 네이밍한 것이 마음에 안 든다. 대체로 Section이라고 네이밍하더라... 코드 자체를 스타일이라고 할 수는 있어도 일반적으로 통용되는 네이밍 방식을 쓰는 게 더 좋을 것 같다.
2. 상위 요소에서 Query를 수행할 필요 없이 Tanstack Query가 동시 요청을 처리해주므로, 각각의 하위 요소에서 호출하면 더 깔끔한 구조가 될 것 같다.
4. 트리거 이벤트 관리
프로젝트 초기에 러프하게 기능 구현을 생각해두고 있을 때는 Zustand Store에서 트리거 이벤트를 관리하면 되지 않을까? 하는 생각을 하고 있었다. 그런데 실제 구현 계획을 세워보니 Store로 이벤트를 관리하는 데에 문제가 있어 보였다.

1차적인 문제는 클라이언트 각각의 컴포넌트의 동작에 대해 이벤트 처리 로직을 추가해줘야 하기 때문에 로직이 흩어지는 문제가 있다.
또한 클라이언트에서 관리할 경우 트리거 동작을 위해 클라이언트에서 유저의 모든 미션 정보를 가지고 있어야 했다. 추가로 모든 미션에 대해서 비교 연산을 클라이언트에서 수행한 뒤 서버로 미션 완료 정보를 보내야 하는 것도 덤이었고.
그러니 서버 쪽에서 관리하던가, 미션 로직을 단순화할 필요가 있었다. 가장 먼저 생각했던 건 서버쪽으로 미션 발급과 미션 트리거 관리를 전부 넘겨버리는 거였다. 이번 프로젝트에서는 서버쪽 작업을 너무 하기 싫었어서 클라이언트쪽에서 뾰족한 방법이 없는지 강사님과 멘토님께 자문을 구했다.
질문 전문
미션, 도전과제 트리거를 어떻게 관리해야 할까요?
프로그램적 문제보다는 구현 방향성에 대한 질문입니다.
저희는 유저가 뽑은 책에 대해 조건에 따라 달성되는 미션(읽은 책에 대해서 리뷰 남기기, 책 요약 남기기, 인상깊은 구문 남기기 등)
그리고 유저의 활동에 대한 조건에 따라 달성되는 도전과제(책 50권 서재에 넣기, 미션 100개 달성하기, 리뷰 50개 남기기)가 있습니다.
그런데 클라이언트단에서 조건을 관리하려고 하니 트리거 동작을 위해 유저 정보 스토어에 담아야 할 정보가 점점 늘어나야 하고, 모든 요청에 따라서 유저 정보 스토어를 항상 관리해야 하는 문제와 각 컴포넌트에 대해서 트리거 로직이 흩어져야 하는 문제가 있습니다. ( 팬아웃이 너무 커질 것 같다는 예상)
[ 팬아웃 사진 ]
그래서 지금 고민하고 있는 부분은 미션이나 도전과제의 종류를 줄이고 트리거 로직을 최소화할지, 유저가 모든 미션에 대해서 완료 처리를 직접 하도록 할지 고민입니다.
트리거 로직을 최소화할 경우에는
- 백엔드에서 구현해야 할지(supabase에서)
- 클라이언트단에서 구현해야 할지
에 대한 고민이 있고
유저가 미션에 대해서 완료 처리를 직접 해야할 경우에는
- 유저가 즉각적인 피드백을 못 느낌
- 컨텐츠의 클라이언트 의존성이 강해짐
에 대한 고민이 있습니다.
클라이언트 트리거로 작동한다면 유저에게 즉각적으로 피드백(애니메이션, 모달 등)을 보여주는 것을 기대했는데, 관리가 너무 어려울 것 같아서 질문드립니다!
DB 구조는 이렇습니다.
[DB 구조 사진]
관련 컴포넌트 구조는 이렇습니다.
[컴포넌트 사진]

팀에서 나온 대안을 요약하면 다음과 같았다. 1, 2는 구현 부하를 줄이는 방법이었고, 3은 원인을 제거하는 방법이었다. 강사님과 멘토님 모두 서버에서 구현하는 방법을 제안해주셨고, 원인 제거를 위해 서버에서 구현하기로 결정했다.
supabase는 Postgre기반이어서 RPC를 생성해 관리하도록 했다. 필요한 함수는 세 가지였다.
1. api_list_book_missions
페이지에 들어갈 때마다, 새로고침할 때마다 미션 목록을 랜덤으로 가져오는 것은 어색하다. 책에는 isbn13이라는 고유한 키값이 있다. 이 키에 해당하는 미션을 확정적으로 가져오기 위해 미션 번들의 id를 해시값으로 가져올 수 있도록 했다.
2. api_assign_book_tasks
해당 isbn13에 대한 미션을 유저에게 수락하는 함수이다.
3. api_process_event
유저의 이벤트에 대해 조건이 일치하는 유저 이벤트가 있는지 파악하고 완료 처리하는 함수이다.
이벤트 분류별 관리를 위해 분리한 함수 api_rule_count_event, api_rule_checklist, api_rule_streak도 있지만, 이 셋은 3번 함수 api_process_event에 의해 호출된다.
그림으로 요약한 구현 흐름은 다음과 같다.
1. 미션 목록, 미션 번들, 번들 목록, 이벤트 로그, 유저 미션, 보상 목록 테이블로 미션을 관리

2. 이벤트 로그에 들어온 정보를 판별해 유저 미션 완료 처리

3. 클라이언트의 realtime event - 소켓 이벤트를 이용해 변경을 파악한 후 렌더링

5. Nginx, Express를 이용한 Proxy 처리
외부 API를 사용하면서 Vercel 배포를 사용할 때의 가장 큰 문제는 외부 API 주소에 화이트리스트를 등록하는 게 불가능하다는 것이다.

Vercel은 동적 IP 주소를 사용하기 때문에 요청하는 IP가 고정적이지 않다. 따라서 요청하는 IP주소가 지속적으로 변경되는데, 이런 경우 문제는 프로젝트가 언제든 멈출 수 있다는 것이다.

내가 그래도 기본적으로 AWS는 쓸 줄 알았고, 아직 무료 플랜도 남아있는 상태여서 서버에 Proxy를 구현해 사용할 수 있었다. 프로젝트 마감 시점에 가까웠었기 때문에 단시간에 처리해야 했었다. 그래서 급하게 처리할 수 밖에 없어 Nginx를 붙여 두기는 했지만 https요청으로 받는 것도 아니었고, certbot에서 인증서도 안 받고 급하게 구현해 어떻게든 문제를 해결할 수 있었다. ( 많이 아쉽다 )

Proxy 요청을 보내는 김에 supabase 요청 처리도 Proxy 서버를 통해 요청하고 싶긴 했지만 마감 직전에 급하게 구현해서 빠르게 필요한 것만 처리할 수밖에 없었다.
5. 개인적인 성장
1. Tanstack Query 학습 및 성공적인 적용
옵션들에 대해 학습한 것은 물론, Tanstack Query의 caching 전략과 쿼리 무효화에 대해서 학습할 수 있었다.
또한 직접 부딪혀보면서 어떤 패턴이 더 좋은가에 대해서 생각하게 되었다. 아래는 내가 프로젝트 진행하면서 Tanstack Query에 대해서 정리한 내용이다.
Tanstack Query의 강력한 점을 꼽자면
- staleTime과 gcTime을 이용한 캐싱, 최신화 관리
- refetchOnWindowFocus와 같은 브라우저 레벨의 유저 동작에서의 제어 옵션
- select 등을 이용한 return 데이터 가공
- isLoading, isPending, isError 등과 같은 다양한 위치에서 제공되는 상태
- defaultSetting으로 전역적인 쿼리 클라이언트 동작 제어
- onMutate, isError Callback 등으로 이벤트 발생 타이밍마다 작동하는 콜백 제어로 낙관적 업데이트 가능
- queryClient 직접 제어를 통한 쿼리 무효화, refetching 등 직접 제어 가능
- Infinity 제어에 특히 강력한 InfinityQuery과 같은 특정 쿼리 동작에 특화된 함수
직접 써본 동작들만 해도 다음과 같다.
직접 박치기 해보면서 작성한 코드와 최종 정착하게 된 코드를 정리해보자
Mutation
완전 초기
// useBookmarkFetching.ts
// 북마크 토글을 처리합니다.
export const useToggleBookmark = (isbn13: string | undefined, uid?: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => {
if (!isbn13) throw new Error("isbn13 is required");
return bookmarkRepo.toggleBookmark(isbn13);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["bookmark", isbn13] });
if (uid) queryClient.invalidateQueries({ queryKey: ["bookmarks", uid] });
},
});
};
문제점
- Mutate Function 호출 시점에 파라미터를 받고 있음
- 이 상태면 mutation 함수 하나에 대해서 항상 id값을 호출해줘야 한다.
// BookDetailPartition.tsx (파일명도 Section으로 변경하는 게 자연스러워 보인다.)
...
// 호출 시점
const { mutate: toggleBookmark, isPending: togglePending } =
useToggleBookmark(id);
...
return( ...
<button onClick={toggleBookmark}>북마크 버튼</button>
위 상태와 같이 각각의 아이템에 대한 mutate 함수를 호출할 때마다 새로 받아야 하므로 좋지 않음. 리스트 아이템이라면 매 리스트 아이템마다 useToggleBookmark를 호출해야 하는 소요 발생.
중반기
위 문제를 인식하고 mutationFn 콜백에 파라미터를 부여해 하나의 mutate 함수를 이용해 여러 아이템을 통제하도록 구현
//useReviewFetching.ts
// 파일과 함께 리뷰를 게시합니다 ( 파일 없어도 상관 없음 )
export const useSetReviewWithFiles = () => {
const qc = useQueryClient();
return useMutation({
mutationKey: ["review", "create"],
mutationFn: (vars: SetReviewType) => reviewRepo.setReviewWithFile(vars),
retry: 0,
onSuccess: (_newReview, vars) => {
logicRpcRepo.setProcessEvent("REVIEW_CREATED", {
book_id: vars.isbn13,
review_id: _newReview.id,
});
qc.invalidateQueries({ queryKey: ["review", "byIsbn", vars.isbn13] });
qc.invalidateQueries({ queryKey: ["review", "byUser", vars.uid] });
},
});
};
호출 시점마다 vars를 부여하여 리스트 아이템에 대해 매번 새로 생성해야 하는 문제 해결
// reviewWritePartition.tsx
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
...
const { isbn13, bookname: title } = data.book;
mutate({
isbn13,
title,
content,
score: rating,
uid: id,
image_file: image,
});
...
};
문제점
- 확장성이 전혀 없는 구조임. 이미 정의된 쿼리키 무효화만 사용 가능. 서로 다른 페이지의 컴포넌트에서에서 호출한다고 가정했을 때 예상치 못한 사이드이펙트 발생 가능
최종 정리
기본적인 동작만 취하면서 기본 옵션은 default에 종속적으로 변경해 반복적인 options 소요를 줄이고 onMutate와 같은 콜백 동작, 옵션들을 쿼리를 사용하는 컴포넌트에서 주입하도록 변경해 유연성을 개선했다.
이 구조의 장점
- state와 같은 상태 변경이나 컴포넌트 요소 통제 동작을 주입 할 수 있다.
- 여러 컴포넌트에서 이 동작을 사용한다고 했을 때 컴포넌트에 따른 유연한 동작을 구현할 수 있다.
// useSummaryFetching.ts
export const useSetSummary = (
options?: UseMutationOptions<unknown, Error, SetSummaryType>
) => {
return useMutation({
mutationKey: ["setSummary"],
mutationFn: async ({ summary, isbn13 }: SetSummaryType) => {
const row = await summaryRepo.setSummary(summary, isbn13);
logicRpcRepo.setProcessEvent("SUMMARY_CREATED", {
book_id: isbn13,
summary_id: String(row.id),
});
return row;
},
...options,
});
};
호출해 사용할 때는 이런 형태가 된다.
// SummaryPartition.tsx
const { mutate } = useSetSummary({
onSuccess: () => {
// 사이드 이펙트 동작을 직접 부여
qc.invalidateQueries({ queryKey: ["getSummary", isbn13] });
},
});
장점만 있는 것은 아니고, 일장일단이 있다. 한번만 호출될 단순한 동작이거나
각각의 컴포넌트에서 호출하더라도 동작이 같아야 할 경우 로직의 응집성이 흩어지거나 중복 작성할 수 있는 문제가 있다.
그래도 일관성 있는 동작을 부여하기 위해 대부분의 useMutate에서 이 동작을 사용할 것 같다. options를 optional로 부여했으니 단순 조회 같은 추가 로직 부여가 필요 없는 경우 옵션을 추가적으로 부여할 필요는 없을 것 같다.
2. 팀 리더로서 소통 주도
멘토링, 특강해주시는 선배 개발자분들이 소통의 중요성을 항상 강조하는 이유를 알 것 같은 프로젝트였다. 팀원분들이 적극 소통해주신 부분도 컸겠지만, 주도적으로 소통을 유도하고 적극적으로 공유되는 작업에 대해서 정돈한 덕에 프로젝트가 더 가치 있게 느껴졌다.
3. 재사용성 있는 코드 작성
위에서 서술한 filter 컴포넌트 뿐만 아니라 목록, 리뷰, 내 서재, 책 정보 페이지에서 재사용 될 수 있는 평점 정보 RatingStar 컴포넌트, Bookmark 컴포넌트 등을 작성했다. 이 컴포넌트들을 작성하면서 재사용성 있는 컴포넌트를 작성하는 방법에 대해 알게 되었다.
컴포넌트 작성 초기에 Props를 모든 상황에 대비해 작성하기보다는 팀원에게 필요한 상황에 대해서 물어보고 그 상황에 맞는 컴포넌트를 만들고, 추가적으로 필요한 상황이 올 경우 지속적으로 관리하며 더 넓은 상황에 대처할 수 있는 컴포넌트가 되어가는 것이 중요한 것 같다고 느꼈다.
6. 좋았던 점
1. 생산성
컴포넌트 공유, 필요한 라이브러리 적극적으로 도입으로 빠르게 기능을 만들어낼 수 있었다.
2. 소통 / 협업
원활한 커뮤니케이션이 가능한 팀원들과, 자유로운 피드백이 가능한 분위기를 조성하는 게 프로젝트 진행에 얼마나 도움이 되는지 알게 되었던 계기가 된 것 같다.
3. 라이브러리
새로운 도구와 라이브러리를 적극 도입해 프로젝트에 녹여낸 것이 좋았다.
7. 아쉬웠던 점
1. 학습
팀 플래그부터 생산성에 직결된 것을 많이 넣었다. 그것에 좀 매몰된 것이 아쉽다. 직접 부딪혀보면서 학습하는 것도 큰 도움이 되었다고 생각하지만, 처음부터 어떤 방법이 좋은 방법인지 잘 알고 있었다면 코드 퀄리티에 더 신경을 쓸 수 있었을 것 같다.
2. 코드 품질
1번과 거의 연결되는 이야기이다. 급하게 기능을 작성한 느낌이 없지않아 있어 응집도 / 결합도 관리가 조금 부족했던 것 같다.
3. 디테일 부족
접근성(aria-label) 관리가 부족했고, 디테일 페이지에 대해서는 반응형 작업을 하지 못했다.
8. 마무리
문서화나 프로젝트 마무리 준비를 적극적으로 같이 진행해주는 팀원들 덕분에 마무리까지 좋았던 프로젝트가 되었던 것 같습니다. 많이 배우는 계기가 되었고, 내 코드에 대해서 더 고민해보고 어떻게 더 좋은 코드를 만들지 고민해보게 되었습니다.
다음 프로젝트에서도 적극적으로 커뮤니케이션하고, 더 좋은 프로젝트를 위해 한 팀이 되는 프로젝트를 하면 좋겠다는 생각이 많이 들었습니다. 다음 프로젝트에서도 Tanstack Query를 적용할 계획인 만큼, 팀원들과 공유할 수 있는 문서를 만들 계획입니다.
학습에 대한 아쉬움은 팀원들과 함께 공유하는 notion에 주기적으로 next나 관련 토픽들을 공유하면서 학습을 진행하면 좋을 것 같습니다.
적극적으로 내 일처럼 도와주신 이성헌 멘토님, 우리 범쌤 덕분에 프로젝트를 열심히 하게 되는 계기가 되었던 것 같습니다 감사합니다!