Skip to content

fix: 조용히 실패하던 인증·계약 결함 수정과 누락 화면 구현 - #86

Open
RosieOh wants to merge 20 commits into
mainfrom
fix/audit-remediation
Open

fix: 조용히 실패하던 인증·계약 결함 수정과 누락 화면 구현#86
RosieOh wants to merge 20 commits into
mainfrom
fix/audit-remediation

Conversation

@RosieOh

@RosieOhRosieOh commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

개요 (Overview)

코드베이스 전반을 점검하면서 화면에 아무 표시가 나지 않아 오래 방치돼 있던 결함을 고치고,
데이터 계층만 있고 화면이 없던 기능을 채웠습니다.

가장 큰 것은 두 가지입니다.

  • 일반 로그인은 동작한 적이 없습니다. 서버는 200 과 토큰을 주는데 응답 스키마가 최상위
    userId 를 필수로 요구해 zod 파싱에서 통째로 버려졌습니다.
  • 새로고침하면 무조건 로그아웃됐습니다./auth/refresh 도 같은 모양이라
    SessionBootstrap 이 파싱 실패를 세션 만료로 보고 clearTokens() 를 불렀습니다.
    인터셉터의 401 → 갱신 → 재시도 도 마지막 단계에서 항상 무너졌습니다.

둘 다 콘솔에도 화면에도 흔적이 남지 않는 종류라, 같은 부류를 계약 테스트로 고정했습니다.

변경 사항 (Changes)

커밋 13개로 나눴고, 각 커밋 본문에 "왜 그게 문제였는지" 를 적었습니다.

보안

  • 응답 인터셉터만 isDevelopment 가드가 빠져 모든 API 응답 본문이 프로덕션 콘솔에 찍혔습니다.
    이 앱은 건강기록·개인정보를 다룹니다
  • 카카오 인가 코드를 console.log 하고 주소창에 남기던 것을 제거

서버 계약

  • 로그인·갱신 응답의 신원은 최상위가 아니라 중첩된 user 에 있습니다
  • 역할 enum 이 ['PARENT','CHILD'] 였습니다. 서버에 CHILD 는 없고 CAREGIVER·ADMIN·
    GUEST 를 거부해 관리자는 로그인 자체가 불가능했습니다
  • 프로필 완성도는 불리언 맵으로 오는데 string[] 을 기다렸습니다. 전 필드가 nullish
    파싱은 통과하고 값만 전부 undefined 가 됐습니다
  • 챗봇 기록은 배열인데 { success, content } 래퍼를 기다렸습니다
  • 무효화 키를 문자열로 손수 적어 ['admin','health-records'] 가 실제
    ['admin','healthRecords',...] 와 어긋났습니다. 전부 팩토리 참조로 바꿔 이름이 바뀌면
    컴파일 에러가 나게 했습니다

동작하지 않던 진입점

  • 글쓰기 FAB 이 검색창 포커스 시에만 떠서 글 목록에서는 글쓰기 진입점이 없었습니다
  • 검색 아이콘 onClick={search} 가 MouseEvent 를 검색어로 넘겨 event.trim() 에서 터졌습니다
  • 챗봇이 모든 사용자를 'test-user-id' 로 전송했습니다
  • 본인 글에 수정·삭제가 뜬 적이 없습니다 (getUserId()user_1787..., authorId 는 DB id)
  • 프로필 수정 화면은 값이 하드코딩돼 있고 저장 버튼에 핸들러가 없어 입력이 그냥 버려졌습니다

라우트·접근성

  • 13개 화면이 레이아웃 그룹 밖에 있었습니다. 그중 /chat 은 하단 탭의 목적지인데도 그룹 밖이라
    챗봇 탭을 누르면 탭 바가 사라져 다른 탭으로 돌아갈 수 없었습니다
  • 탭 바를 nav + Link 로 재작성 (프리페치·aria-current·새 탭 열기)
  • IconButtonaria-label 을 필수 prop 으로 올려 12개 호출부에 이름 부여
  • focus-visible 이 저장소 전체에 0곳, focus:outline-none 만 6곳이었습니다 (WCAG 2.4.7)
  • SessionBootstrap 의 첫 렌더가 서버/클라이언트에서 달라 모든 페이지에서 hydration 이
    깨지고
    있었습니다

새 화면

  • /children/[childId]/edit · /mypage/blocked · /chat/history
  • 댓글·리뷰 수정·삭제, 알림 개별 읽음·삭제, 관리자 정책 검증 버튼
  • 북마크한 지원금 목록, 안 읽은 알림 배지, 동의 이력, 건강 알림, 프로필 완성도 안내
  • useBlockUser 는 호출부가 0개라 애초에 아무도 차단할 수 없었습니다

정리·인프라

  • 선언 없이 호이스팅으로만 동작하던 의존성 6개 명시, 미사용 패키지 제거
  • 레거시 훅 6개와 그 아래 계층(API·쿼리 키·스키마) 제거 (-398줄)
  • CI(typecheck·lint·test·build)와 pre-commit 훅 추가
  • .env.example 추가 — .gitignore.env* 때문에 커밋 자체가 불가능했습니다

변경 유형 (Change Type)

  • 🐛 Bug Fix: 버그 수정
  • ✨ Feature: 새로운 기능 추가
  • 💄 UI/UX: 사용자 인터페이스 변경
  • ♻️ Refactor: 코드 리팩토링 (기능 변경 없음)
  • 🧪 Test: 테스트 추가 또는 수정
  • 🔧 Config: 구성 파일 변경
  • 📝 Docs: 문서 변경
  • 🗑️ Chore: 기타 변경사항 (빌드 스크립트, 종속성 등)

기술적 상세 설명 (Technical Details)

React.VFC@types/react 19 에서 삭제된 타입입니다.src/types/syg.d.ts(파일명도 오타)가
이를 쓰고 있어 tsc --noEmit 이 실패하고 있었습니다. CI 가 없어 아무도 모르고 있었습니다.

flat config 는 node_modules 만 기본 제외합니다.next lint(15.3 에서 deprecated)를
eslint . 로 바꾸면서 .next 를 무시하지 않으면 빌드 산출물까지 훑느라 린트가 끝나지 않습니다.

개발용 로그인 버튼은 카카오 앱 키 없이 로그인 뒤 화면을 확인할 수 없어 추가했습니다.
NODE_ENV 분기(빌드 시 상수 치환 → 죽은 코드 제거)와 환경변수 주입 두 겹으로 막혀 있고,
환경변수를 채운 채 프로덕션 빌드를 돌려도 번들에 이메일·비밀번호·컴포넌트 이름이 남지 않는 것을
확인했습니다.

백엔드와 짝인 변경이 하나 있습니다.PUT /children/{id} 는 전체 교체인데 specialNeeds
(알레르기·기저질환)가 응답에 없어 수정 화면이 현재 값을 읽어올 방법이 없었습니다. 이름만 고쳐도
특이사항이 지워집니다. 백엔드는 CareCode_Interfacefix/child-special-needs-response
브랜치(a877d76)에서 응답에 필드를 추가했고, 이 PR 의 c749a21 이 그 짝입니다.
백엔드 PR: CareCode-Repo/CareCode_Interface#96먼저 머지·배포돼야 합니다.
그 전까지는 아이 수정 화면에서 특이사항이 비어 보이고, 저장하면 지워집니다.

체크리스트 (Checklist)

  • 빌드가 정상적으로 통과됐습니다.
typecheck 통과
lint 통과 (경고 0)
test 189 통과 (175 → +14)
build 통과, 44 pages

추가 정보 (Additional Information)

동작 확인 방법 — 실제 백엔드·MariaDB 를 띄우고 브라우저(CDP)로 구동해 확인했습니다.

cp .env.example .env.local # NEXT_PUBLIC_API_URL 을 백엔드 주소로# 개발 계정을 만들고 .env.local 의 NEXT_PUBLIC_DEV_LOGIN_* 에 넣으면# 로그인 화면에 [DEV] 버튼이 생깁니다 (README 참고)
npm run dev

확인한 것: 로그인 → /home 이동, 하드 내비게이션 후 세션 유지(= 새로고침 로그아웃 해소),
/children·/mypage·/notification/settings·/health 전 화면 콘솔 오류 0·4xx/5xx 0,
아이 수정에서 이름만 바꿔 저장했을 때 특이사항 보존, 본인 댓글에만 수정·삭제 노출.

리뷰 시 유의할 점

  • 프로필 이미지 업로드는 넣지 않았습니다. 백엔드 PUT /users/me/profile-imageURL 문자열만
    받고 파일 업로드 엔드포인트가 없어, 동작하지 않는 UI 를 만들지 않았습니다.
  • 관리자 하드 삭제 훅은 그대로 뒀습니다. README 가 "지우면 기록이 사라진다" 며 의도적으로
    버튼을 만들지 않은 것으로 읽었습니다.
  • 커밋을 기능 단위로 나눠 두었으니 커밋별로 보시는 편이 읽기 쉽습니다.

import 하면서 package.json 에 없던 패키지가 6개 있었다. framer-motion 은 motion 의,
radix 서브패키지 5개는 radix-ui 모놀리스의 전이 의존성 호이스팅으로만 동작했다.
npm 에서는 우연히 되지만 pnpm·yarn PnP 로 옮기면 그날로 빌드가 깨진다.
- 직접 import 하는 radix 서브패키지 5개를 선언하고 쓰지 않는 radix-ui 를 제거
- 코드·CSS 어디에서도 쓰지 않는 @skeletonlabs/skeleton 2종 제거
- firebase 를 정확 버전으로 고정. 서비스 워커가 CDN URL 에 버전을 박아 쓰므로
캐럿을 두면 번들과 조용히 어긋난다
- next lint 는 15.3 에서 deprecated 라 eslint 를 직접 호출.
flat config 는 node_modules 만 기본 제외해서, .next 를 무시하지 않으면
빌드 산출물까지 훑느라 린트가 끝나지 않는다
- CI(typecheck·lint·test·build)와 pre-commit 훅 추가.
README 는 pre-commit 이 있다고 적혀 있었지만 실제로는 commit-msg 뿐이었다
- .env.example 추가. .gitignore 의 .env* 때문에 커밋 자체가 불가능했다
응답 인터셉터만 isDevelopment 가드가 빠져 있어 모든 API 응답 본문이 프로덕션
콘솔에 찍혔다. 이 앱은 건강기록·개인정보를 다루므로 그대로 두면 안 된다.
- printResponseConsole 을 개발 환경으로 제한 (요청·에러는 이미 그랬다)
- 개발 로그의 Authorization 헤더를 마스킹. 붙었는지만 알면 되고,
전문을 찍으면 화면 공유·녹화에 그대로 남는다
- 카카오 인가 코드 console.log 제거하고 history.replaceState 로 주소창에서 제거.
일회용이지만 크리덴셜이라 히스토리·리퍼러에 남길 이유가 없다
- 콜백에서 push 대신 replace 를 쓴다. 뒤로 가기로 돌아오면 소진된 코드로 재시도한다
서버 TokenDto 는 최상위 userId/email/role 을 채우지 않는다. 신원은 항상 중첩된
user 안에 있는데(AuthServiceImpl.issueTokenForUser) 스키마가 최상위를 필수로 요구해
두 가지가 망가져 있었다.
- 일반 로그인: 200 과 토큰을 받고도 zod 파싱에서 실패해 한 번도 성공한 적이 없다
- 세션 복구: /auth/refresh 도 같은 모양이라 SessionBootstrap 이 파싱 실패를
세션 만료로 보고 clearTokens 를 불렀다. 새로고침할 때마다 로그아웃됐고
인터셉터의 401 재시도도 마지막 단계에서 항상 무너졌다
그 밖에 같은 부류로 드러난 것들:
- 역할 enum 이 ['PARENT','CHILD'] 였다. 서버에 CHILD 는 없고 CAREGIVER·ADMIN·GUEST 를
거부해 관리자는 로그인 자체가 불가능했다. user.ts 의 USER_ROLE 을 정본으로 모았다
- 프로필 완성도는 completionPercentage 와 불리언 맵으로 오는데 completionRate 와
string[] 을 기다렸다. 전 필드가 nullish 라 파싱은 통과하고 값만 전부 undefined 였다
- 챗봇 기록은 배열인데 { success, content } 래퍼를 기다렸고, isHelpful 은 null 이 온다.
userId 는 서버가 토큰에서 꺼내므로 파라미터로 보내도 무시된다
- 아이 응답에 specialNeeds 추가 (백엔드 a877d76 과 짝)
SessionBootstrap 의 첫 렌더도 함께 고쳤다. useState(() => hasStoredSession() ...) 는
서버에서 localStorage 를 읽을 수 없어 서버는 children, 클라이언트는 대기 화면을 그려
모든 페이지에서 hydration 이 깨졌다. 저장소 판단은 effect 안에서만 한다.
문자열로 적은 무효화 키가 실제 키와 어긋나 아무것도 잡지 못하는 곳이 있었다.
- ['admin','health-records'] 는 실제 ['admin','healthRecords',...] 와 달라
관리자 건강기록을 지워도 목록이 갱신되지 않았다
- ['facility','my-bookings'] 도 ['facility','myBookings',...] 와 어긋나
예약을 확정·반려해도 사용자의 내 예약이 그대로였다
나머지는 우연히 맞고 있었지만 키 이름을 바꾸는 순간 같은 방식으로 깨진다.
전부 팩토리 참조(_def / .queryKey)로 바꿔 이름이 바뀌면 컴파일 에러가 나게 했다.
정책을 고칠 때 검증률 지표도 함께 무효화한다.
13개 화면이 (with-tabs)·(without-tabs) 어디에도 속하지 않아 어떤 레이아웃도 받지
못했다. 그중 /chat 은 하단 탭의 목적지인데도 그룹 밖이라, 챗봇 탭을 누르는 순간
탭 바가 통째로 사라져 다른 탭으로 돌아갈 수 없었다.
- /chat 을 (with-tabs) 로, notification·policy·search/policy·signup·mypage/edit 을
(without-tabs) 로 이동
- 화면이 아니라 통과 지점인 auth 콜백과 자체 레이아웃이 있는 admin 은 그대로 둔다
- 내용이 <div>/register</div> 뿐인 스텁 라우트 제거
탭 바는 Radix Tabs + router.push 였다. 페이지 이동을 tablist/tab 으로 표현해
스크린리더가 존재하지 않는 tabpanel 을 찾았고, Link 가 아니라 뷰포트 프리페치와
새 탭 열기가 동작하지 않았다. nav + Link 로 바꾸고 활성 탭은 aria-current 로 알린다.
활성 상태도 state + useEffect 대신 pathname 에서 파생해, 첫 렌더에 항상 첫 탭이
켜졌다가 뒤늦게 바뀌던 깜빡임을 없앴다.
아이콘만 있는 버튼은 접근 가능한 이름이 없어 스크린리더에 "버튼" 하나로만 읽혔다.
IconButton 의 aria-label 을 필수 prop 으로 올려 12개 호출부가 모두 이름을 갖게 했다.
- focus-visible 링 복원. 저장소 전체에 focus-visible 이 0곳이고 focus:outline-none 만
6곳 있어 키보드 사용자는 지금 어디에 있는지 알 수 없었다(WCAG 2.4.7)
- Button 에 type="button" 기본값. 폼 안의 보조 버튼이 제출을 일으켰다
- 테마에 없어 Tailwind 기본 팔레트로 새던 red-400 제거
- manifest 와 앱 아이콘 추가. 서비스 워커가 없는 /images/logo.png 를 가리키고 있었다
- 쓰지 않는 Skeleton CSS import 와 정의된 적 없는 --font-geist-* 제거,
Pretendard 폰트 스택을 테마 토큰으로 올려 font-pretendard 유틸리티가 실제로 생기게 함
- React.VFC 는 @types/react 19 에서 삭제된 타입이다. FC 로 교체하고
파일명 오타(syg.d.ts)도 함께 고쳤다
- 글쓰기 FAB 이 searchFocused 일 때만 떴다. 그때는 목록 대신 최근 검색어가 보이므로
정작 글 목록에서는 글쓰기 진입점이 없었다. 조건을 뒤집었다
- 검색 아이콘의 onClick={search} 는 MouseEvent 를 검색어로 넘겨 event.trim() 에서
터졌다. 아이콘 클릭이 아예 동작하지 않고 엔터만 살아 있었다
- NewPostFAB 이 router.push('community/write') 로 상대 경로를 썼다.
진입 지점에 따라 목적지가 달라진다
- 챗봇이 모든 사용자를 'test-user-id' 로 전송했다. 훅이 getUserId() 로 직접 읽게 하고
호출부가 식별자를 넘길 여지를 없앴다. 에러 분기도 문자열 매칭 대신 상태 코드로 본다
- 커뮤니티 목록의 최근 검색어가 하드코딩 더미였다. /search 와 같은 저장소를 쓴다
- 앱 셸이 h-dvh 컨테이너인데 h-screen 을 써서 스크롤이 두 겹이던 화면 정리
- 랜딩의 min-h-[852px] min-w-[393px] 제거. 특정 기기 해상도를 못 박아
그보다 작은 화면에서 가로 스크롤이 생겼다
- 중복 구현하던 IntersectionObserver 를 공용 훅으로 모으고, 옵션 객체가 매 렌더
새로 만들어져 옵저버가 다시 생성되던 문제를 막았다
- zustand devtools 를 개발 환경으로 제한
카카오 로그인은 실제 앱 키와 등록된 리다이렉트 URI 가 있어야 해서 로컬에서 쓸 수 없다.
그래서 로그인 뒤 화면(아이 관리·건강 기록·마이페이지)을 전혀 확인할 수 없었다.
- usePostLogin 추가. 기존에 postLogin 은 있었지만 이를 쓰는 훅이 없었다.
토큰을 메모리에 넣는 것까지 훅에서 끝내 호출부가 빠뜨릴 여지를 없앤다
- DevLoginButton 은 두 겹으로 막힌다. NODE_ENV 는 빌드 시 상수로 치환돼 분기 전체가
죽은 코드가 되고, 계정은 환경변수로만 들어와 값이 없으면 렌더되지 않는다.
환경변수를 채운 채 프로덕션 빌드를 돌려도 번들에 이메일·비밀번호·컴포넌트 이름이
남지 않는 것을 확인했다
- 회원가입 폼의 인라인 rules 를 제거하고 zod 스키마를 단일 출처로 삼는다.
@hookform/resolvers 대신 25줄짜리 어댑터를 두어 의존성을 늘리지 않았다
훅은 만들어져 있는데 어떤 화면도 쓰지 않는 항목이 30개 있었다. 그중 실제로 빠져
있던 화면과 기능을 채웠다.
새 화면
- /children/[childId]/edit — 아이 정보 수정. 등록 화면만 있고 수정이 없었다
- /mypage/blocked — 차단한 사용자 관리. useBlockUser 는 호출부가 0개라
애초에 아무도 차단할 수 없었다. 게시글 메뉴에 차단을 붙여 진입점도 만들었다
- /chat/history — 지난 상담 내역. 세션을 펼칠 때만 그 기록을 받는다
기존 화면에 빠져 있던 것
- 프로필 수정이 완전히 죽어 있었다. 값을 하드코딩하고 저장 버튼에 핸들러가 없어
입력이 그대로 버려졌다. 프로필을 불러와 채우고 저장하도록 다시 만들었다
- 댓글 수정·삭제 (API 자체가 없어 PUT/DELETE 추가)
- 시설·병원 리뷰 수정·삭제 (공용 ReviewItem)
- 알림 개별 읽음·삭제. 알림을 열면 딥링크로 이동해버려 열지 않고 읽음 처리할
방법이 필요했다
- 관리자 정책 검증 표시·해제. 사용자 화면의 확정/추정 금액을 가르는 버튼인데 없었다
작성자 판별도 함께 고쳤다. getUserId() 는 user_1787... 인데 authorId 는 DB id 라
항상 거짓이었고, 본인 글에도 수정·삭제가 뜬 적이 없다. 서버가 자리마다 다른
식별자를 쓰므로(시설 리뷰만 업무 userId, 나머지는 DB id) useCurrentUser 가 둘을
함께 돌려주고 각 자리에서 맞는 쪽을 고른다.
- 북마크한 지원금 목록을 나의 활동 탭에 추가. 지원금 북마크는 핵심 기능인데
저장한 것을 다시 볼 화면이 없어 기능이 반만 있었다
- 안 읽은 알림을 종 아이콘에 표시. showBadge prop 도 조회 훅도 있었지만 실제
화면에서 아무도 넘기지 않아(갤러리에서만 썼다) 알림함에 들어가 보기 전까지
알 수 없었다. 상단바가 있는 다섯 화면이 같은 훅을 쓰고 키가 같아 요청은 한 번만 나간다
- 동의 이력을 개인정보 화면에 추가. 약관은 개정되므로 "지금 동의했는가" 만으로는
무엇에 동의했는지 증명할 수 없다. 펼쳤을 때만 조회한다
- 건강 알림(다가오는 검진·접종)을 건강 기록 상단에 노출. 푸시는 놓치면 끝이라
화면에 들어온 사람에게 다시 보여준다
- 프로필 완성도 안내. 주소가 비면 지역별 지원금과 주변 시설 추천이 아예 동작하지
않는데 알려주는 곳이 없었다. 빠진 항목을 이름으로 알려주고 수정 화면으로 보낸다
세 가지 모두 데이터가 없으면 아무것도 그리지 않는다.
레거시 훅 6개와 그 아래 계층(API 함수·쿼리 키·zod 스키마·import)을 함께 지웠다.
훅만 지우면 아래가 고아로 남는다.
- useGetKakaoAuthUrl (mutation 버전이 실사용) / usePostSignup (카카오 가입으로 대체)
- useGetPolicyList (검색으로 대체) / useFacilitiesByKeyword (시설 검색으로 대체)
- useNotificationDetail (목록이 모든 필드를 준다) / useUpdateNickname (프로필 저장이 겸한다)
- 참조가 0인 RightContent·StoreHeader·TabNavigation, dummy-policies,
주석 처리된 채 남아 있던 KakaoLoginButton
getKakaoAuthUrl API 함수는 mutation 이 계속 쓰므로 남겼다. 훅과 함께 지웠다가
typecheck 가 잡아 되돌렸다.
관리자 하드 삭제 훅(useDeleteAdminUser·useDeleteAdminBooking)은 남긴다.
지우면 누가 언제 취소했는지 기록이 사라져 의도적으로 버튼을 만들지 않은 것이다.
화면에 아무 표시가 나지 않아 오래 방치됐던 것들이라 픽스처는 전부 실제 서버 응답을
그대로 옮겼다.
- postLoginResponseSchema / postRefreshTokenResponseSchema
최상위 userId 가 null 이어도 통과하고 신원은 user 에서 읽는지
- getProfileCompletionResponseSchema
missingFields 가 불리언 맵이고 완성 여부 키가 complete 인지
- 인터셉터 401 → 갱신 → 재시도
동시 401 에도 갱신이 한 번만 나가는지(single-flight), 재시도가 한 번으로 끝나는지,
갱신 API 자체의 401 을 갱신 대상으로 오해하지 않는지
175 → 189.
코드에서 읽어낼 수 없고 다시 밟기 쉬운 것만 적었다.
- 토큰 응답의 신원은 항상 user 안에 있다 (로그인·갱신이 조용히 깨져 있던 이유)
- 작성자 판별은 어느 식별자인지 확인하고 쓴다 (자리마다 다르다)
- 아이 수정은 전체 교체라 현재 값을 모두 읽어와 채운 뒤 저장해야 한다
- 프로필 완성도 응답은 불리언 맵이다
- 첫 렌더는 서버와 클라이언트가 같아야 한다 (hydration)
- import 하는 패키지는 반드시 선언한다
- 하단 탭의 목적지는 반드시 (with-tabs) 안에 둔다
- 개발용 빠른 로그인 사용법과 계정 만드는 법
첫 CI 실행이 테스트 단계에서 죽었다. 코드가 아니라 워크플로 설정 문제다.
jsdom 30 은 Node ^22.22.2 || ^24.15.0 을, 그 아래 undici 8 은 >=22.19.0 을 요구한다.
Node 20 에서는 undici 가 webidl.util.markAsUncloneable 을 찾지 못해 jsdom 로딩부터
터지고 테스트가 한 건도 돌지 않는다(13 errors, no tests).
- 워크플로를 Node 24 로 고정 (로컬 개발 환경과 같은 메이저)
- package.json 에 engines 를 명시해 낮은 버전에서 npm ci 가 경고를 내게 함
@RosieOhRosieOh added bug 버그 수정 (fix) enhancement 새로운 기능 추가 (feat) documentation 문서 변경 (docs) refactor 기능 변경 없는 코드 리팩토링 ci CI 설정 변경 labels Aug 24, 2026
@RosieOh
RosieOhforce-pushed the fix/audit-remediation branch from 530e1f8 to b17985bCompareAugust 24, 2026 01:56
라우트가 52개인데 loading.tsx 가 하나도 없었다. 각 화면에 자체 스켈레톤은 있지만
그건 데이터를 기다리는 동안의 것이고, 그보다 앞선 구간 — 라우트의 JS 청크를 받아오는
동안 — 은 화면 컴포넌트 자체가 없어서 아무것도 그려지지 않았다.
- 그룹별 loading.tsx. 공용 Loading 은 body 스크롤을 잠그는 모달용이라 쓰지 않고
RouteSkeleton 을 따로 뒀다
- 그룹별 error.tsx. 루트 하나만 있으면 화면 하나가 깨져도 레이아웃까지 날아가
사용자가 다른 탭으로 옮겨갈 수단조차 사라진다
- global-error.tsx. 루트 레이아웃 자체가 터지면 지금까지 Next 기본 화면이 나왔다.
이 경계는 레이아웃을 대신하므로 html·body 를 직접 그리고 인라인 스타일만 쓴다
- (without-tabs)/not-found.tsx. 상세 화면에서 없는 리소스를 열었을 때
홈이 아니라 이전 목록으로 돌려보낸다
52개 라우트 중 21개만 로그인이 필요하다. 지원금·시설·병원·게시글 상세는 로그인 없이
열리는 공개 문서인데 sitemap 도 OG 이미지도 없어 검색·공유 유입 경로를 통째로 쓰지
않고 있었다.
- sitemap: 정적 경로 + 정책·게시글·시설·병원 상세. 빌드 시점에 백엔드가 떠 있지
않을 수 있어(CI 등) 실패하면 정적 경로만 내보낸다. 목록당 500건으로 끊는다
- robots: 로그인이 필요한 화면과 관리자 화면은 제외. 색인돼 봐야 검색 결과에서
로그인 화면으로 튕긴다
- OG 이미지: 루트는 브랜드 카드, 지원금 상세는 제목을 실어 링크만 보고도 무슨
지원금인지 알 수 있게 한다. 외부 폰트를 받아오지 않는다 — 폰트 서버가 느리면
이미지 생성이 통째로 실패한다
주소는 NEXT_PUBLIC_SITE_URL 로 주입한다.
검색은 "무엇을 찾을지 아는 사람" 만 쓸 수 있다. 지원금이 서비스의 핵심 가치인데
키워드 검색뿐이라, 뭐가 있는지 모르는 사용자가 발견할 길이 없었다.
- /policy/category/[category] — 카테고리별 지원금 목록. 다른 카테고리로 바로
건너뛸 수 있게 칩을 함께 둔다
- 육아 정보 탭에 카테고리 칩과 "많이 찾는 지원금" 추가
- 건강 기록에 월령 기준 넛지. 서버가 추천 정책을 이름 문자열로만 주므로(id 가 없다)
상세가 아니라 검색 지름길로 쓴다 — 없는 링크로 404 를 만드는 것보다 낫다
- 커뮤니티에 태그 지름길. 태그로 거르는 API 가 없어 태그 이름으로 검색을 태운다
- 시설·병원 목록에 "많이 찾는" 줄. 표본이 없으면 아무것도 그리지 않는다
`getCommunityTags` 는 서버가 주는 객체 배열을 z.array(z.string()) 으로 파싱해
부를 때마다 실패하고 있었다. 그래서 이 훅은 한 번도 쓰인 적이 없다. 스키마를 맞췄다.
백엔드에 API 가 없어 못 만들던 두 가지다.
(백엔드: CareCode-Repo/CareCode_Interface#98 — 먼저 배포돼야 동작한다)
- /mypage/liked-hospitals — 찜한 병원 목록. 병원 상세에서 찜은 걸 수 있는데
모아 볼 곳이 없었다. 마이페이지에서 들어간다
- 프로필 사진 업로드. EditProfileImage 는 파일을 base64 로 바꿔 넘기고 있었지만
그걸 받아 줄 경로가 없어 어디에도 저장되지 않았고, 그래서 어느 화면에서도
쓰이지 않는 컴포넌트였다. 파일을 그대로 올리고 서버가 준 주소를 화면에 반영한다
업로드 응답의 `/files/...` 는 **백엔드 오리진** 기준 경로라 그대로 쓰면 프런트 주소로
해석돼 404 가 난다. toAbsoluteFileUrl 로 붙여 준다. next/image 는 unoptimized 로 둔다 —
외부 호스트를 최적화 대상에 넣으려면 remotePatterns 설정이 필요한데, 저장소가 S3 로
바뀌면 그 설정도 함께 바뀌어야 해서 지금 묶어 두지 않는다.
이미지·용량 검증은 클라이언트에서 먼저 거른다(서버 multipart 상한 10MB 와 맞춤).
첨부 링크가 `<a href="/files/...">` 였다. 그 경로는 프런트 주소로 해석되고, 백엔드
주소로 바꿔도 인증이 필요해 401 이다. 업로드는 되는데 한 번도 열어본 적이 없었다.
(백엔드: CareCode-Repo/CareCode_Interface#99 — 먼저 배포돼야 동작한다)
- 인증된 axios 로 본문을 받아 blob 으로 다룬다. 파일명 클릭은 내려받기,
이미지는 object URL 로 목록에서 바로 미리보기
- object URL 은 언마운트에서 해제한다. 그러지 않으면 화면을 오갈 때마다 blob 이 쌓인다
본문 조회는 useEffect 가 아니라 쿼리로 감쌌다. 직접 받으면 같은 첨부에 요청이 렌더마다
중복돼(개발 모드에서 1개 첨부에 4번 나갔다) 캐시·중복 제거가 필요하다. 남은 2번은
StrictMode 이중 호출이라 프로덕션에는 없다.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug버그 수정 (fix)ciCI 설정 변경documentation문서 변경 (docs)enhancement새로운 기능 추가 (feat)refactor기능 변경 없는 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@RosieOh