Web-Front 실전 케이스북

사용법

이 문서는 00~13 문서를 다시 읽게 만드는 실전 동선이다. 정답을 외우는 자료가 아니라, 실제 개발 중 만날 법한 장면을 보고 어떤 문서를 열어야 하는지 떠올리는 연습장에 가깝다.

모든 케이스를 한 번에 끝내려 하지 않는다. 하루에 2~3개만 읽고, 먼저 생각해보기를 잠깐 떠올린 뒤 찾아볼 문서를 열어보면 충분하다. 이미 아는 내용처럼 보여도 관찰할 증거를 기준으로 다시 보면, 개발 중 어디서 확인해야 하는지가 몸에 남는다.

각 케이스는 세 가지 감각을 섞는다.

  • 상황 진단형: 화면, Network, Console, Application, DevTools에서 무엇을 볼지 잡는다.
  • 설계 선택형: 기능을 만들기 전에 어떤 기본값과 경계를 정할지 잡는다.
  • 리뷰 감각형: 코드나 API 계약을 보고 어떤 위험 신호를 말할지 잡는다.

커버리지 지도

문서연결되는 Case주로 회수하는 감각
00. Web-Front 학습 로드맵Case 01, 최종 미션학습 순서, 학습 깊이, 전체 문서 지도
01. 백엔드 개발자를 위한 프론트엔드 실전 판단Case 01, Case 02, Case 03, Case 04, Case 05, Case 06, Case 07, Case 08, Case 09, Case 12, Case 19, Case 20, Case 24, Case 27, Case 28저장소, 에러, API 계약, URL, 성능, 테스트 판단
02. 백엔드 개발자를 위한 프론트엔드 문법 함정과 디버깅Case 10, Case 13, Case 14, Case 15, Case 16, Case 17, Case 20, Case 21, Case 22, Case 23, Case 27, Case 28JS/TS/React/CSS/env 실전 함정
03. 브라우저와 Web 기본Case 04, Case 18, Case 19브라우저 제약, CORS, Cookie, Web Storage
04. JavaScript TypeScriptCase 10, Case 13, Case 17비동기, 타입 경계, Date, input, nullable
05. ReactCase 05, Case 14, Case 15, Case 16state snapshot, effect, key, 렌더링 경계
06. API와 서버 상태Case 05, Case 06, Case 07, Case 08, Case 09, Case 20, Case 28서버 상태, query key, invalidation, error state
07. Form Validation UXCase 10, Case 11, Case 12, Case 26, 최종 미션form 값 변환, 서버 field error, submit UX
08. Auth SecurityCase 02, Case 03, Case 04, Case 18, 최종 미션token, Cookie, XSS, CSRF, refresh
09. Routing Layout NavigationCase 19, Case 20, 최종 미션URL 상태, protected route, query param
10. Styling UI Design SystemCase 21, Case 22, Case 23, Case 24, Case 26layout, overlay, component state, 디자인 시스템
11. Performance AccessibilityCase 25, Case 26성능 측정, 이미지, semantic HTML, focus
12. Build Deploy ObservabilityCase 04, Case 27, 최종 미션env, build artifact, cache, source map, trace
13. Testing CollaborationCase 11, Case 25, Case 28, 최종 미션MSW, e2e, API 계약, fixture

케이스 목록

인증과 보안

  • Case 01. 어디부터 읽어야 할지 막막한 첫날
  • Case 02. 빠르게 만들려고 token을 localStorage에 넣으려 한다
  • Case 03. 로그인은 성공했는데 새로고침하면 다시 로그인 화면으로 간다
  • Case 04. 401 refresh가 동시에 여러 번 호출되거나 반복된다
  • Case 05. 로컬에서는 되던 Cookie 인증이 운영에서 흔들린다

API와 서버 상태

  • Case 06. 서버 데이터를 useState에 복사했더니 화면마다 값이 달라진다
  • Case 07. 보관 버튼을 눌렀는데 목록이 그대로다
  • Case 08. 좋아요는 바로 바뀌는데 실패하면 화면이 거짓말을 한다
  • Case 09. API는 실패했지만 UI가 무엇을 보여줘야 할지 모른다
  • Case 10. 검색, 필터, 정렬, 페이지가 서로 엇갈린다

Form과 Validation

  • Case 11. 나이를 비웠는데 0처럼 제출된다
  • Case 12. 서버 field error가 input 아래에 붙지 않는다
  • Case 13. 버튼을 한 번 눌렀는데 주문이 두 번 생성된다

React와 TypeScript 함정

  • Case 14. API 응답에 as User를 붙였는데 운영에서 깨진다
  • Case 15. 검색어를 바꿨는데 결과가 예전 그대로다
  • Case 16. 알림 숫자가 interval 안에서 멈춘다
  • Case 17. 목록에서 행을 삭제했더니 다른 input 값이 움직인다
  • Case 18. 화면은 빈칸인데 에러는 없다

브라우저와 Routing

  • Case 19. Postman에서는 되는데 브라우저 fetch는 실패한다
  • Case 20. URL에 넣은 값이 로그와 공유 링크에 남는다
  • Case 21. 뒤로가기를 눌렀는데 검색 조건이 복원되지 않는다

Styling과 UI 디버깅

  • Case 22. 긴 제목 하나가 카드와 테이블 레이아웃을 밀어낸다
  • Case 23. dropdown이 card 밖으로 열리지 못하고 잘린다
  • Case 24. sticky toolbar와 모바일 하단 버튼이 어색하게 밀린다
  • Case 25. Button, Modal, Table이 화면마다 다르게 행동한다

성능, 접근성, 배포, 테스트

  • Case 26. 목록 화면이 느리고 이미지가 늦게 흔들린다
  • Case 27. 클릭은 되지만 keyboard와 테스트가 모두 불안하다
  • Case 28. 운영 env를 바꿨는데 예전 API URL을 계속 호출한다
  • Case 29. 테스트는 통과하지만 실제 API에서는 깨진다

Case 01. 어디부터 읽어야 할지 막막한 첫날

상황

프론트 문서가 00~13까지 정리되어 있지만, 전부 한 번에 읽으려니 시작 전부터 피로하다. 지금 필요한 것은 모든 기술 이름을 암기하는 일이 아니라, 개인 프로젝트를 만들 때 어떤 문서를 언제 꺼낼지 감각을 잡는 일이다. 로그인, API 연동, form, styling, 배포 중 어디서 막힐지 아직 모르기 때문에 전체 지도를 먼저 봐야 한다.

관찰할 증거

  • 지금 만들 프로젝트에 로그인, form, 목록, 검색, 배포가 있는지 확인한다.
  • 막히는 지점이 문법인지, 브라우저 제약인지, API 계약인지, UI 상태인지 나눈다.
  • 00의 문서 지도와 학습 순서가 실제 프로젝트 순서와 맞는지 본다.

가져갈 한 문장

처음부터 모두 외우려 하지 말고, 지금 만난 상황을 기준으로 열어볼 문서를 고른다.

Case 02. 빠르게 만들려고 token을 localStorage에 넣으려 한다

상황

개인 프로젝트 로그인 기능을 만들고 있다. 빠른 구현을 위해 access token과 refresh token을 모두 localStorage에 저장하려는 생각이 든다. 기능은 금방 붙을 것 같지만, 사용자 정보와 관리자 기능이 들어갈 가능성이 있다.

관찰할 증거

  • token이 access token인지 refresh token인지 구분한다.
  • 사용자 입력 HTML 렌더링, third-party script, dependency 사용 여부를 본다.
  • refresh token 탈취 시 계정 장악이나 장기 세션 탈취로 이어지는지 판단한다.
  • Cookie를 쓰면 CORS credentials, SameSite, CSRF 방어가 필요한지 확인한다.

가져갈 한 문장

저장소 선택은 편의성 문제가 아니라 XSS, CSRF, refresh 권한, 배포 도메인을 함께 보는 설계 결정이다.

Case 03. 로그인은 성공했는데 새로고침하면 다시 로그인 화면으로 간다

상황

로그인 API는 200을 반환했고 /me도 한 번은 성공했다. 그런데 새로고침하면 protected route가 다시 로그인 화면으로 보낸다. 로컬에서는 가끔 괜찮고, 운영에서는 더 자주 풀리는 것처럼 보인다.

관찰할 증거

  • Application 탭에서 refresh Cookie가 실제로 저장되었는지 본다.
  • /auth/refresh 또는 /me 요청에 Cookie가 실리는지 Network request header를 확인한다.
  • response의 Set-Cookie 속성에서 SameSite, Secure, Domain, Path를 본다.
  • refresh 실패 후 query cache와 사용자 상태가 정리되는지 본다.

가져갈 한 문장

새로고침 인증 문제는 route보다 Cookie 저장 여부와 요청 첨부 여부를 먼저 본다.

Case 04. 401 refresh가 동시에 여러 번 호출되거나 반복된다

상황

대시보드 진입 시 여러 API가 동시에 401을 받는다. 각 요청이 자기 방식으로 refresh를 호출하면서 refresh API가 여러 번 날아간다. 어떤 경우에는 refresh endpoint도 401을 반환하고, interceptor가 다시 refresh를 시도해 흐름이 반복된다.

관찰할 증거

  • Network에서 refresh 요청이 몇 번 발생하는지 본다.
  • refresh endpoint 자체가 자동 refresh 대상에서 제외되어 있는지 확인한다.
  • 401과 403을 구분하는지, refresh 실패 응답이 일관적인지 본다.
  • TanStack Query retry와 auth refresh retry가 서로 겹치는지 확인한다.

가져갈 한 문장

401 refresh는 화면별 예외 처리가 아니라 인증 client의 동시성 제어 문제다.

상황

로컬에서는 로그인 유지가 잘 된다. 운영 배포 후에는 Cookie가 저장되지 않거나 API 요청에 실리지 않는다. 프론트는 credentials: "include"를 넣었고, 백엔드는 CORS를 열었다고 말한다.

관찰할 증거

  • 프론트 origin과 API origin의 scheme, host, port를 확인한다.
  • same-origin과 same-site를 구분한다.
  • response header의 Access-Control-Allow-Origin, Access-Control-Allow-Credentials, Vary: Origin을 본다.
  • SameSite=None이면 Secure와 HTTPS가 함께 있는지 확인한다.
  • preflight OPTIONS가 인증 미들웨어에서 막히지 않는지 확인한다.

가져갈 한 문장

Cookie 인증 디버깅은 credentials 한 줄이 아니라 origin, site, CORS, Cookie 속성의 조합을 보는 일이다.

Case 06. 서버 데이터를 useState에 복사했더니 화면마다 값이 달라진다

상황

프로젝트 목록을 여러 화면에서 사용한다. 한 화면에서 프로젝트를 보관했는데 다른 사이드바의 프로젝트 개수는 그대로다. 각 컴포넌트가 API 응답을 자기 useState에 복사해 들고 있다.

관찰할 증거

  • 같은 서버 데이터가 몇 개의 local state에 복사되어 있는지 찾는다.
  • 데이터의 source of truth가 서버인지 브라우저 UI인지 나눈다.
  • mutation 후 어떤 query나 화면이 stale해지는지 본다.
  • loading, error, empty, success 상태가 컴포넌트마다 반복 구현되어 있는지 확인한다.

가져갈 한 문장

상태 관리의 첫 질문은 “이 값의 source of truth가 서버인가, 브라우저인가”다.

Case 07. 보관 버튼을 눌렀는데 목록이 그대로다

상황

프로젝트 목록에서 보관 버튼을 눌렀다. API는 200을 반환했고 서버 데이터도 바뀌었다. 하지만 현재 목록에는 보관된 프로젝트가 계속 남아 있고, 새로고침해야만 사라진다.

관찰할 증거

  • mutation 성공 후 어떤 query key를 invalidate하는지 확인한다.
  • 목록 query key가 filter, status, page를 포함하는지 본다.
  • 상세, 목록, 사용자 summary 중 같이 갱신되어야 할 데이터가 있는지 확인한다.
  • optimistic update를 했다면 rollback 스냅샷이 있는지 본다.

가져갈 한 문장

mutation을 만들 때는 API 호출보다 “성공 후 어떤 화면이 stale해지는가”를 같이 설계한다.

Case 08. 좋아요는 바로 바뀌는데 실패하면 화면이 거짓말을 한다

상황

사용자가 좋아요를 누르면 화면을 즉시 바꾸고 싶다. optimistic update를 적용했더니 UX는 빨라졌지만, 서버 실패 후에도 화면에는 좋아요가 유지된다. 결제나 재고 차감에도 같은 방식으로 적용할지 고민된다.

관찰할 증거

  • 실패 시 이전 cache로 rollback할 수 있는 스냅샷이 있는지 확인한다.
  • 실패 비용이 낮은 상호작용인지, 금액/권한/재고처럼 보수적으로 다뤄야 하는지 나눈다.
  • mutation 실패 code와 충돌 처리 정책이 백엔드에 있는지 본다.
  • 성공 후 서버 최신 상태를 재조회할 기준이 있는지 확인한다.

가져갈 한 문장

낙관적 UI는 “빠르게 보여주기”가 아니라 “실패해도 되돌릴 수 있는가”의 문제다.

Case 09. API는 실패했지만 UI가 무엇을 보여줘야 할지 모른다

상황

서버가 에러를 반환한다. 프론트는 모든 실패를 “요청 실패” toast 하나로 보여준다. 사용자는 다시 로그인해야 하는지, 값을 고쳐야 하는지, 권한이 없는지, 잠시 후 재시도해야 하는지 알 수 없다.

관찰할 증거

  • error response에 code, message, fieldErrors, traceId가 있는지 확인한다.
  • 401, 403, validation error, permission denied, server error가 구분되는지 본다.
  • field error와 form-level error, global toast를 나누는지 확인한다.
  • 서버 내부 예외 문장이 사용자 화면에 노출되는지 본다.

가져갈 한 문장

에러 응답은 “실패했다”가 아니라 “사용자가 다음에 무엇을 해야 하는가”를 알려줘야 한다.

Case 10. 검색, 필터, 정렬, 페이지가 서로 엇갈린다

상황

프로젝트 목록에 검색어, 상태 필터, 정렬, 페이지네이션이 모두 들어갔다. 필터를 바꿨는데 이전 page가 유지되어 빈 화면이 나오고, sort 기준이 바뀌면 다음 페이지에서 중복 row가 보인다. API와 프론트가 page index와 sort 표현을 다르게 이해하고 있다.

관찰할 증거

  • query param과 API parameter 이름이 같은지 확인한다.
  • page가 0 기반인지 1 기반인지 본다.
  • query key가 search, filter, sort, page를 모두 포함하는지 확인한다.
  • 검색 조건 변경 시 page나 cursor를 초기화하는지 본다.
  • totalCount, hasNext, nextCursor, sort 안정성이 응답에 있는지 확인한다.

가져갈 한 문장

목록 화면은 검색 조건, URL, query key, API 계약이 하나의 세트다.

Case 11. 나이를 비웠는데 0처럼 제출된다

상황

회원가입 form에서 나이를 비웠다. 그런데 제출 payload에는 age: 0처럼 들어가거나 validation이 기대와 다르게 통과한다. input type은 number라서 당연히 number라고 생각했다.

관찰할 증거

  • Network payload에서 실제 값이 string인지 number인지 확인한다.
  • 빈 문자열이 z.coerce.number()에서 어떻게 처리되는지 본다.
  • state가 입력 중 draft string인지, 제출용 domain number인지 나뉘어 있는지 확인한다.
  • NaN이 payload로 나가는지 확인한다.

가져갈 한 문장

입력 중인 값은 UI draft이고, 서버로 보낼 값은 검증을 통과한 domain 값이어야 한다.

Case 12. 서버 field error가 input 아래에 붙지 않는다

상황

회원가입 API가 400과 함께 fieldErrors를 내려준다. 그런데 프론트는 이 에러를 모두 toast로만 보여주거나, 서버 field name을 as keyof Form으로 밀어 넣는다. 백엔드가 field 이름을 바꾸면 프론트는 조용히 매핑에 실패한다.

관찰할 증거

  • 서버 응답의 field path가 프론트 form field와 일치하는지 본다.
  • 알 수 없는 field가 왔을 때 form-level error로 보여주는 fallback이 있는지 확인한다.
  • field error, form-level error, toast error를 구분하는지 본다.
  • 테스트에서 서버 validation error를 실제 사용자 문구로 검증하는지 확인한다.

가져갈 한 문장

서버 field error는 toast가 아니라 사용자가 고칠 input으로 돌아가야 한다.

Case 13. 버튼을 한 번 눌렀는데 주문이 두 번 생성된다

상황

사용자는 결제 버튼을 한 번 눌렀다고 말한다. 로그를 보면 같은 주문 생성 API가 짧은 시간에 두 번 호출되었다. 프론트는 버튼 disable을 넣었지만, 네트워크 지연, 뒤로가기, 재시도, 새로고침까지 막지는 못했다.

관찰할 증거

  • submit 중 isSubmitting이나 mutation pending 상태로 버튼이 비활성화되는지 본다.
  • 버튼 label이 처리 중 상태를 보여주는지 확인한다.
  • 주문, 결제, 가입 같은 API에 idempotency key가 있는지 본다.
  • 같은 요청이 재시도될 때 서버 응답 정책이 있는지 확인한다.

가져갈 한 문장

중복 submit은 프론트 UX와 백엔드 멱등성이 함께 막는다.

Case 14. API 응답에 as User를 붙였는데 운영에서 깨진다

상황

/me 응답을 as User로 단언하고 바로 사용했다. 개발 데이터에서는 문제가 없었지만 운영에서 profile이 빠진 계정이 들어오자 화면이 터졌다. TypeScript가 있으니 안전하다고 생각했지만, 런타임 JSON은 타입 검사를 거치지 않았다.

관찰할 증거

  • 외부 입력을 unknown으로 받는지, 바로 as로 단언하는지 본다.
  • OpenAPI 생성 타입을 쓰는 경우 실제 응답과 문서가 일치하는지 확인한다.
  • nullable과 optional이 API 타입에 정확히 표현되어 있는지 본다.
  • Cannot read properties of undefined 주변에 as, any, !가 있는지 검색한다.

가져갈 한 문장

as는 값을 안전하게 만드는 코드가 아니라, 이미 안전하다고 주장하는 코드다.

Case 15. 검색어를 바꿨는데 결과가 예전 그대로다

상황

검색 화면에서 keyword prop이 바뀐다. 하지만 effect dependency array가 비어 있어 첫 검색 결과만 계속 보인다. dependency를 추가하니 이번에는 요청이 너무 자주 반복되거나, 먼저 보낸 요청이 늦게 도착해 최신 결과를 덮어쓴다.

관찰할 증거

  • effect 안에서 읽는 props와 state가 dependency에 들어 있는지 본다.
  • keyword가 바뀔 때 이전 요청을 abort하거나 무시하는지 확인한다.
  • 검색어 debounce와 서버 부하 정책을 정했는지 본다.
  • 이 서버 상태를 직접 effect로 다룰지 TanStack Query로 옮길지 판단한다.

가져갈 한 문장

검색 effect의 핵심은 dependency, 취소, 최신성, 서버 상태 도구의 경계를 같이 보는 것이다.

Case 16. 알림 숫자가 interval 안에서 멈춘다

상황

알림 숫자를 1초마다 증가시키는 interval을 만들었다. 그런데 숫자는 1에서 멈추거나, 오래된 count를 계속 기준으로 계산한다. console을 찍어보면 callback이 현재 state가 아니라 처음 렌더링 때의 값을 보고 있다.

관찰할 증거

  • interval, timeout, subscription callback이 어떤 렌더링 시점의 값을 캡처했는지 본다.
  • 이전 state로 다음 state를 만들면서 functional update를 쓰는지 확인한다.
  • 최신 값을 읽어야 하는지, 이전 값에서 다음 값을 계산해야 하는지 나눈다.
  • cleanup이 있는지 확인한다.

가져갈 한 문장

callback 안의 state가 항상 최신이라고 믿지 말고, 캡처 시점과 업데이트 방식을 확인한다.

Case 17. 목록에서 행을 삭제했더니 다른 input 값이 움직인다

상황

할 일 목록에서 두 번째 항목을 삭제했더니 세 번째 항목에 입력 중이던 값이 다른 행으로 옮겨간다. React warning은 없거나 무시하고 있었다. list key로 index를 사용했기 때문에 React가 item identity를 잘못 재사용하고 있다.

관찰할 증거

  • list key가 index인지, 안정적인 서버 id인지 확인한다.
  • reorder, insert, delete가 가능한 목록인지 본다.
  • 임시 생성 항목에 client id가 있는지 확인한다.
  • row 내부에 input state나 focus 상태가 있는지 본다.

가져갈 한 문장

목록 key는 “잘 렌더링되게 하는 값”이 아니라 “같은 item임을 증명하는 값”이다.

Case 18. 화면은 빈칸인데 에러는 없다

상황

사용자 프로필 화면의 제목이 빈칸이다. 콘솔 에러도 없고 API도 200이다. 코드를 보니 필수 데이터처럼 보이는 값마다 optional chaining이 붙어 있어 계약 위반이 조용히 undefined로 숨겨지고 있다.

관찰할 증거

  • UI 깊은 곳에 ?.가 반복되는지 검색한다.
  • query loading, error, empty 경계에서 데이터를 좁힌 뒤 컴포넌트에 넘기는지 본다.
  • nullable과 optional이 API 계약에서 구분되어 있는지 확인한다.
  • view model 변환 지점이 있는지 본다.

가져갈 한 문장

필수 데이터에 붙은 ?.는 에러를 없애는 대신 문제를 숨길 수 있다.

Case 19. Postman에서는 되는데 브라우저 fetch는 실패한다

상황

백엔드 API는 Postman에서 정상 응답한다. 브라우저 fetch에서는 CORS 에러가 보이고, Network에는 OPTIONS 요청이 먼저 보인다. 백엔드는 “서버는 200을 줬다”고 말하지만, 프론트 코드는 응답 본문을 읽지 못한다.

관찰할 증거

  • 실제 요청 전에 preflight OPTIONS가 성공하는지 확인한다.
  • Access-Control-Allow-Origin, allowed headers, allowed methods를 본다.
  • credentials 요청이면 Access-Control-Allow-Credentials와 특정 origin 허용을 확인한다.
  • custom header나 JSON content type이 preflight를 유발하는지 본다.

가져갈 한 문장

Postman 성공은 브라우저 성공의 증거가 아니며, CORS는 브라우저가 강제하는 계약이다.

Case 20. URL에 넣은 값이 로그와 공유 링크에 남는다

상황

비밀번호 재설정 token, access token, 임시 form 값, 이메일 같은 정보를 URL query param에 넣는 구현을 보았다. 공유와 새로고침 복원에는 편하지만, 브라우저 history, 서버 로그, 분석 도구, referrer에 남을 수 있다.

관찰할 증거

  • URL에 민감 정보가 포함되어 있는지 확인한다.
  • 해당 URL이 history, access log, analytics, referrer로 흘러갈 수 있는지 본다.
  • query param에 둘 상태와 local state로 둘 상태를 나눈다.
  • search param 값의 타입 변환과 기본값 처리를 확인한다.

가져갈 한 문장

URL에 넣는 값은 “공유되어도 되는 상태”인지 먼저 확인한다.

Case 21. 뒤로가기를 눌렀는데 검색 조건이 복원되지 않는다

상황

목록 화면에서 검색어와 상태 필터를 바꿨다. 뒤로가기를 누르거나 새로고침하면 검색 조건이 사라지고 기본 목록으로 돌아간다. 사용자는 방금 보던 목록을 다시 찾기 어렵다.

관찰할 증거

  • search, filter, sort, page가 URL query param에 들어 있는지 본다.
  • query key가 URL 상태와 같은 값을 사용하는지 확인한다.
  • query param 변경 시 page reset과 scroll reset이 필요한지 본다.
  • 모달이나 임시 form 값처럼 URL에 넣지 않아야 할 상태와 구분한다.

가져갈 한 문장

뒤로가기와 새로고침에서 살아야 하는 목록 상태는 URL과 query key에 같이 들어간다.

Case 22. 긴 제목 하나가 카드와 테이블 레이아웃을 밀어낸다

상황

프로젝트 제목이 길게 들어오자 카드가 부모 너비를 밀어내고, table은 모바일에서 화면 밖으로 크게 삐져나간다. CSS에 overflow: hiddentext-overflow: ellipsis를 넣었지만 말줄임이 동작하지 않는다.

관찰할 증거

  • flex/grid child 또는 중간 컨테이너에 min-width: 0이 있는지 확인한다.
  • 긴 URL, code block, table 같은 intrinsic size가 큰 자식이 있는지 본다.
  • DevTools Layout panel에서 실제 box size와 overflow를 확인한다.
  • 백엔드 fixture에 긴 제목, 빈 값, 긴 URL 같은 깨지는 데이터가 있는지 본다.

가져갈 한 문장

긴 텍스트가 레이아웃을 밀면 min-width: 0, overflow 전략, 모바일 정보 우선순위를 함께 본다.

Case 23. dropdown이 card 밖으로 열리지 못하고 잘린다

상황

카드 안의 메뉴 버튼을 누르면 dropdown이 열려야 한다. 하지만 카드에 overflow: hidden이 있어 dropdown이 카드 경계에서 잘린다. z-index를 크게 올려도 위로 올라오지 않는 경우도 있다.

관찰할 증거

  • 부모 chain에 overflow: hidden/auto/scroll이 있는지 확인한다.
  • transform, opacity, filter, isolation, positioned element가 stacking context를 만드는지 본다.
  • overlay가 portal로 body 아래에 렌더링되는지 확인한다.
  • layer scale을 token으로 관리하는지 본다.

가져갈 한 문장

overlay가 안 보이면 z-index보다 overflow chain과 stacking context를 먼저 추적한다.

Case 24. sticky toolbar와 모바일 하단 버튼이 어색하게 밀린다

상황

모바일 화면에서 상단 toolbar는 sticky로 붙어야 하고, 하단 제출 버튼은 안전 영역 위에 있어야 한다. 그런데 주소창이 접히거나 input focus로 키보드가 올라오면 버튼이 잘리거나 toolbar가 붙지 않는다. 100vh를 쓰면 화면이 맞는 듯하다가 실제 기기에서 어긋난다.

관찰할 증거

  • sticky 요소의 top과 가장 가까운 scroll container를 확인한다.
  • 부모 overflow가 sticky 기준을 바꾸는지 본다.
  • 100vh 대신 100dvh, min-height, safe-area padding을 검토한다.
  • 실제 모바일 기기나 mobile viewport에서 input focus 상태를 확인한다.

가져갈 한 문장

모바일 full-height UI는 CSS 한 줄보다 scroll container, dynamic viewport, safe area의 조합이다.

Case 25. Button, Modal, Table이 화면마다 다르게 행동한다

상황

개인 프로젝트 화면은 기능은 되지만 제품처럼 느껴지지 않는다. 어떤 버튼은 loading이 없고, 어떤 modal은 Esc로 닫히지 않으며, table마다 empty와 error 상태가 다르다. 서버 enum이 추가되자 badge가 조용히 깨진다.

관찰할 증거

  • Button, Input, Modal, Toast, Badge, Table의 상태 목록이 있는지 본다.
  • Button 기본 type과 submit 버튼의 의도가 명시되어 있는지 확인한다.
  • 서버 enum이 label, color, icon, disabled 정책으로 매핑되는지 본다.
  • destructive action에 confirm modal과 soft/hard delete 정책이 있는지 확인한다.

가져갈 한 문장

좋은 UI의 출발점은 장식보다 Button, Input, Modal, Table의 상태 일관성이다.

Case 26. 목록 화면이 느리고 이미지가 늦게 흔들린다

상황

프로젝트 목록 화면이 느리다. Network를 보니 목록 1페이지에 수천 row가 내려오고, 카드 썸네일은 원본 3MB 이미지를 그대로 사용한다. Lighthouse 점수보다 실제 사용자가 스크롤하고 검색할 때의 답답함이 더 크게 느껴진다.

관찰할 증거

  • Network에서 API 응답 크기와 이미지 크기를 확인한다.
  • Performance panel에서 JavaScript 실행, layout, paint 병목을 나눈다.
  • 이미지에 width/height, 적절한 썸네일 URL, cache header가 있는지 본다.
  • 목록 API에 pagination과 필요한 field만 내려주는 projection이 있는지 확인한다.

가져갈 한 문장

느린 화면은 React만 보지 말고 API 크기, 이미지, cache, 렌더링을 같이 측정한다.

Case 27. 클릭은 되지만 keyboard와 테스트가 모두 불안하다

상황

카드형 버튼을 div onClick으로 만들었다. 마우스로는 잘 동작하지만 keyboard로 tab 이동이 어렵고, Testing Library 테스트도 class나 DOM depth에 의존한다. form input에는 label이 없고, modal을 닫은 뒤 focus가 어디로 가는지 알 수 없다.

관찰할 증거

  • 버튼처럼 동작하는 요소가 실제 button인지 확인한다.
  • input과 label, error message가 연결되어 있는지 본다.
  • modal open/close 후 focus 이동을 keyboard로 확인한다.
  • 테스트가 role, label, text 기반 query를 사용하는지 본다.

가져갈 한 문장

semantic HTML은 접근성, UX, 테스트 안정성을 동시에 올리는 가장 싼 선택이다.

Case 28. 운영 env를 바꿨는데 예전 API URL을 계속 호출한다

상황

운영 API URL을 바꾸기 위해 배포 플랫폼의 env 값을 수정했다. 하지만 사용자의 브라우저는 여전히 예전 API URL을 호출한다. 급한 마음에 VITE_SECRET 같은 값을 프론트 env에 넣어도 되는지 고민하고, Sentry에는 에러가 찍히지만 어느 배포부터 깨졌는지와 서버 로그의 traceId를 연결하기 어렵다.

관찰할 증거

  • Vite 정적 배포인지, SSR 런타임인지 확인한다.
  • import.meta.env 값이 build 시점에 bundle에 들어갔는지 본다.
  • VITE_ prefix 값 안에 API secret, private key, DB credential처럼 노출되면 안 되는 값이 있는지 확인한다.
  • CDN에서 HTML과 hashed JS asset의 cache 정책을 구분한다.
  • API base URL에 /api/ path prefix가 있을 때 URL 조합 helper가 prefix를 보존하는지 확인한다.
  • Sentry release version, source map 업로드, 서버 응답의 traceId가 같은 장애 분석 흐름으로 연결되는지 본다.

가져갈 한 문장

브라우저가 실행하는 것은 현재 서버 env가 아니라 그때 만들어져 배포된 bundle이다.

Case 29. 테스트는 통과하지만 실제 API에서는 깨진다

상황

컴포넌트 테스트와 e2e smoke test는 통과한다. 하지만 운영 API에서는 nullable 필드가 다르게 오고, validation error 구조도 mock과 다르다. 프론트 테스트는 안심을 주었지만 실제 API 계약과 어긋나 있었다.

관찰할 증거

  • MSW handler가 OpenAPI나 실제 fixture에서 관리되는지 확인한다.
  • success response뿐 아니라 validation error, 401/403, empty, pagination 끝 상태가 mock에 있는지 본다.
  • 테스트가 DOM 구조보다 사용자 role, label, text 중심인지 확인한다.
  • e2e 실패 시 screenshot, trace, network log로 원인을 나눌 수 있는지 본다.
  • 프론트가 답답해하는 API 지점이 error schema, nullable, enum, pagination, timezone 중 어디인지 정리한다.

가져갈 한 문장

프론트 테스트의 신뢰도는 mock이 실제 API 계약과 얼마나 가까운지에 달려 있다.

최종 미션. 작은 프로젝트를 설계해보기

시나리오

작은 프로젝트 관리 앱을 만든다. 기능은 로그인/로그아웃, 회원가입 form, 프로젝트 목록, 프로젝트 생성/수정, 검색/filter/page, protected route, 배포 env, 최소 테스트다. 목표는 멋진 기능을 많이 넣는 것이 아니라, 00~13에서 배운 판단 기준을 실제 설계에 연결하는 것이다.

결정할 것

  • token 저장소는 무엇으로 할 것인가?
  • 401, 403, refresh 실패는 각각 어떤 UI 행동으로 이어질 것인가?
  • 회원가입 form에서 client validation과 server validation의 경계는 어디인가?
  • 서버 field error schema는 어떤 모양인가?
  • 프로젝트 목록의 query key는 search, filter, page를 어떻게 포함하는가?
  • 생성/수정/보관 mutation 후 어떤 query를 invalidate할 것인가?
  • URL query param에 둘 상태와 local state로 둘 상태는 무엇인가?
  • loading, empty, error, permission denied UI를 어떻게 나눌 것인가?
  • 긴 제목, 빈 이미지, 권한별 disabled 상태를 어떤 fixture로 확인할 것인가?
  • env, API base URL, Cookie domain, CORS origin은 환경별로 어떻게 표로 정리할 것인가?
  • unit, component, e2e smoke test는 각각 어떤 최소 흐름만 둘 것인가?

가져갈 한 문장

좋은 개인 프로젝트는 기능 목록이 아니라 선택 기준과 위험 신호가 코드에 남아 있는 프로젝트다.