회고
[우테코] 22편 : 오픈미션 16일차
| 서론
안녕하세요 팡일입니다!
오늘은 우아한테크코스 8기 프리코스의 오픈미션 16일차 포스팅입니다. 오늘은 진행한 점들에 대해서 기록하고 공유하려고 합니다!
| 오늘 한 일
1) QA
QA 진행하기
받은 QA 정리하기
2) 마이페이지
수정 전 입력 필드의 focus 제거하기
회원탈퇴 관련 에러 토스트 직관적으로 수정하기
3) 기타
로그아웃, 로그인, 수정, 삭제 공유 버튼 등의 cursor을 point로 변경하기
파비콘 수정하기
리스트 페이지로 돌아가는 버튼 헤더에 추가하기
4) 리스트 페이지
작성 모드 기능 고민하기
감정 칩 배치 개선
삭제 시, 감정 버튼과 삭제 체크가 겹치는 부분 수정
5) 작성 페이지
배경색과 이모지 색이 겹치는 부분 개선
KPT, DAILY, CUSTOM 등 회고 작성 종류 모드 구분해서 기능 추가하기
6) 디테일/공유 페이지
선택한 감정만 표시되도록 수정
| 어려웠던 점 & 배운 점
1) 유연하지 못한 아키텍처의 대가: 단단한 결합 구조의 리팩토링 경험
(1) 어려움과 마주한 문제
유저 테스트를 통해서 받았던 피드백 중 회고 방식의 다양성에 대해 고민한 결과, KPT 회고를 추가하기로 결정했습니다. 이를 두고, “KPT 폼 하나 추가하면 되겠지?”라는 안일한 생각은 예상치 못한 대규모 리팩토링으로 이어졌습니다. 회고 데이터의 형태를를 저정의하고 있는 RetrospectPayload 타입은 회고 내용을 기존 형태로만 정의하고 있었기 때문에, Keep, Problem, Try를 포함하는 KPT 구조를 표현할 수 없었습니다. 이를 해결하기 위해 DailyRetrospect와 KPTRetrospect의 유니언 타입으로 확장했지만, 그 여파는 retrospectService의 addRetrospect 함수까지 퍼졌습니다. Firestore에 데이터를 저장할 때마다 payload.type을 분기 처리해야 했고, 이는 애플리케이션 전체가 서로 강하게 얽힌 단단한 결합(Tightly Coupled) 구조임을 드러냈습니다.
(2) 개선 과정 및 배운 점
문제 해결을 위해 데이터 구조를 근본부터 다시 설계했습니다. content 필드를 단순한 string이 아니라, 회고 타입에 따라 확장 가능한 객체 형태로 정의했습니다. 예를 들어, content: { today : string, problem : string, learned : string, tomorrow : string, summary : string } 또는 content: { keep: string, problem: string, try: string }처럼 유연하게 표현할 수 있도록 한 것입니다. 이 과정을 통해 “지금 당장의 편의가 미래의 기술 부채로 돌아온다”는 교훈을 얻었고, 이후부터는 기능 설계 시 type 필드를 추가하고, content는 언제든 구조를 확장할 수 있도록 객체 기반으로 설계하는 습관을 갖게 되었습니다.
2) 컴포넌트의 책임 분리(SRP): ‘똑똑한’ 컴포넌트가 버그를 만든다
: KPT 회고 기능을 추가한 직후, 임시저장 기능에서 예기치 않은 버그가 발생했습니다. KPT 회고를 작성하다가 페이지를 벗어나 다시 돌아오면, 이전에 작성했던 Daily 회고의 임시저장이 불쑥 나타나는 이상한 현상이었습니다. 처음에는 단순한 데이터 혼선이라고 생각했지만, 디버깅을 거치며 문제의 근본적인 원인이 명확히 드러났습니다.
Firestore 쿼리가 단순히 “현재 사용자의 임시저장 글”만 요청하고 있었기 때문이었습니다. type 구분 없이 데이터를 가져오다 보니, 데이터베이스는 찾은 문서를 반환했고, 그 결과 Daily 회고가 KPT 페이지에 등장하는 황당한 상황이 발생했습니다. 문제는 여기서 끝나지 않았습니다. WriteContainer.svelte 컴포넌트가 데이터 로딩, 상태 관리, 렌더링 등 모든 역할을 혼자 떠안고 있었기 때문입니다. 이 컴포넌트는 onMount 내에서 URL 파라미터를 읽고, Firestore에서 데이터를 직접 불러오며, writeStore의 상태까지 수동으로 갱신했습니다. 결과적으로 하나의 컴포넌트에 책임이 과도하게 집중되면서 로직이 얽히고, 버그가 발생할 수밖에 없는 구조가 되었습니다.
(2) 개선 과정 및 배운 점
이 문제를 해결하기 위해 두 가지 방향의 리팩토링을 진행했습니다. getDraft 함수를 통해 해당 사용자가 임시 저장한 내용이 있는지 확인한 뒤, 해당 문서의 type이 현재 작성하고자 하는 회고 방식의 type과 동일한지 확인한 뒤, 임시 저장된 글이 있음을 알리는 로직으로 변경했습니다. 이 단순한 변경만으로 Daily와 KPT 간 데이터 충돌이 완전히 사라졌습니다. 둘째, 컴포넌트의 책임을 명확히 분리했습니다. 데이터 로딩과 초기화 로직을 페이지 단으로 올려 src/routes/write/[type]/+page.svelte의 load() 함수에서 처리하도록 변경했습니다. 이제 페이지가 URL의 params.type을 이용해 daily 또는 kpt 키로 데이터를 구분해 불러오고, WriteContainer는 그저 상위에서 지정한 type을 상태관리로 데이터를 참고하기만 하면 됐었습니다.
이 리팩토링을 통해 WriteContainer는 복잡한 책임을 제거하자 코드의 흐름이 훨씬 명료해지고, 유지보수성 역시 크게 향상되었습니다. 이 경험은 “데이터는 명확하게 요청하고, 컴포넌트는 단순하게 유지하라”는 교훈을 남겼습니다. 또한 상태를 가능한 한 상위로 끌어올리는(Lift State Up) 구조가 왜 중요한지를 체감하게 된 순간이었습니다.
3) 디테일의 힘: 작은 불편(Paper Cut)이 완성도를 결정한다
(1) 어려움과 마주한 문제
큰 기능에 집중하다 보면 사소한 불편함을 간과하기 쉽습니다. 그중에서도 기능적으로는 문제가 없지만, 사용자 경험을 떨어뜨리는 ‘Paper Cut’들이 누적되어 사용자 경험(UX)을 해치게 됩니다. 예를 들어, src/routes/mypage/+page.svelte의 회원정보 수정 페이지에서 ‘수정’ 버튼을 누르기 전에도 <input> 필드에 커서가 활성화되는 어색한 UI가 있었습니다. 또한 authHandler.ts의 deleteUserAccount 함수는 Firebase 재인증이 필요한 경우에도 단순히 “오류가 발생했습니다”라는 메시지만 표시했습니다. 사용자는 왜 실패했는지, 무엇을 해야 하는지 알 수 없었습니다.
(2) 개선 과정 및 배운 점
첫 번째 문제는 disable={!isEditing} 속성을 동적으로 제어하여 해결했습니다. 수정 모드에서만 입력 가능하게 하자 사용자는 현재 상태를 직관적으로 인식할 수 있었습니다. 두 번째 문제는 catch 블록에서 error.code를 분석하여, auth/requires-recent-login일 경우 “보안을 위해 재로그인 후 다시 시도해주세요”라는 명확한 안내 메시지를 띄웠습니다. 이러한 작은 수정들이 쌓여 서비스의 완성도를 크게 높였고, “디테일이 완성도를 만든다”는 원칙을 다시금 확신하게 되었습니다.
| 내일 목표
1) QA
QA 진행하기
받은 QA 정리하기
2) 리스트 페이지
정렬 버튼 밖에 클릭하면 닫히게 수정하기
내용 검색 기능 고민하기
3) 작성 페이지
마크다운 에디터 가이드라인 고민하기
감정 커스텀에 대해서 고민하기
글 별 카테고리 추가에 대해서 고민하기
4) 디테일 페이지
수정, 삭제 공유 버튼 개선하기
5) 로그인 페이지
비밀번호 찾기 기능 고민하기
| 한 줄 회고
: "기존의 것을 개선하는 것은 어렵지만 그만큼 뿌듯하다"