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 28 | JS/TS/React/CSS/env 실전 함정 |
| 03. 브라우저와 Web 기본 | Case 04, Case 18, Case 19 | 브라우저 제약, CORS, Cookie, Web Storage |
| 04. JavaScript TypeScript | Case 10, Case 13, Case 17 | 비동기, 타입 경계, Date, input, nullable |
| 05. React | Case 05, Case 14, Case 15, Case 16 | state 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 UX | Case 10, Case 11, Case 12, Case 26, 최종 미션 | form 값 변환, 서버 field error, submit UX |
| 08. Auth Security | Case 02, Case 03, Case 04, Case 18, 최종 미션 | token, Cookie, XSS, CSRF, refresh |
| 09. Routing Layout Navigation | Case 19, Case 20, 최종 미션 | URL 상태, protected route, query param |
| 10. Styling UI Design System | Case 21, Case 22, Case 23, Case 24, Case 26 | layout, overlay, component state, 디자인 시스템 |
| 11. Performance Accessibility | Case 25, Case 26 | 성능 측정, 이미지, semantic HTML, focus |
| 12. Build Deploy Observability | Case 04, Case 27, 최종 미션 | env, build artifact, cache, source map, trace |
| 13. Testing Collaboration | Case 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의 문서 지도와 학습 순서가 실제 프로젝트 순서와 맞는지 본다.
먼저 생각해보기
- 내가 지금 당장 구현할 화면은 무엇인가?
- 그 화면은 API, form, auth, routing, styling 중 어디에 가장 의존하는가?
- 처음부터 깊게 읽어야 할 문서와 막힐 때 돌아와도 되는 문서는 무엇인가?
찾아볼 문서
정리
이 문서 묶음은 순서대로 완주해야만 쓸 수 있는 교재가 아니다.
01은 선택 기준,02는 버그 감지기,03~13은 상황별 원리와 실전 예시다. 개인 프로젝트를 시작할 때는 먼저01,02로 판단 언어를 만들고, 구현 중 만나는 주제 문서로 돌아가는 방식이 가장 부담이 적다.
가져갈 한 문장
처음부터 모두 외우려 하지 말고, 지금 만난 상황을 기준으로 열어볼 문서를 고른다.
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 방어가 필요한지 확인한다.
먼저 생각해보기
- access token과 refresh token 중 더 조심해야 할 값은 무엇인가?
HttpOnlyCookie를 쓰면 어떤 위험은 줄고, 어떤 설정은 새로 필요해지는가?- 단순 개인 프로젝트라는 이유만으로 저장소 결정을 미뤄도 되는가?
찾아볼 문서
정리
localStorage는 구현이 쉽지만 JavaScript가 읽을 수 있으므로 XSS 상황에서 token 탈취로 이어질 수 있다. 특히 refresh token은 재발급 권한이므로 더 엄격한 저장소와 서버 무효화 정책이 필요하다. Cookie 기반 인증은 CSRF, SameSite, CORS를 함께 봐야 하지만, 민감한 세션을 오래 유지해야 한다면 더 안전한 기본값이 될 수 있다.
가져갈 한 문장
저장소 선택은 편의성 문제가 아니라 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와 사용자 상태가 정리되는지 본다.
먼저 생각해보기
- Cookie가 저장되지 않은 문제인가, 저장됐지만 요청에 첨부되지 않는 문제인가?
- 새로고침 복구를
/me로 할지/refresh로 할지 정했는가?- protected route는 API 보안 경계인가, UX 경계인가?
찾아볼 문서
정리
새로고침 후 인증 복구는 route 코드만의 문제가 아니다. 브라우저가 Cookie를 저장하고, 다음 요청에 첨부하고, refresh 실패 시 프론트 상태를 정리하는 흐름이 모두 맞아야 한다. protected route는 UX 제어일 뿐이고, 실제 권한 검사는 API가 계속 담당해야 한다.
가져갈 한 문장
새로고침 인증 문제는 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가 서로 겹치는지 확인한다.
먼저 생각해보기
- refresh는 컴포넌트마다 처리해야 하는가, API client 계층에서 처리해야 하는가?
- 동시에 여러 401이 나면 어떤 요청 하나만 refresh를 담당해야 하는가?
- refresh 실패 후 어떤 cache와 UI 상태를 비워야 하는가?
찾아볼 문서
정리
refresh는 인증 client 계층에서 한 번만 처리하는 편이 안전하다. 단일 refresh promise를 공유하면 동시에 여러 API가 401을 받아도 refresh 폭주를 줄일 수 있다. refresh endpoint는 자동 재발급 대상에서 제외하고, 실패하면 query cache와 사용자 상태를 정리해 로그인 화면으로 보내야 한다.
가져갈 한 문장
401 refresh는 화면별 예외 처리가 아니라 인증 client의 동시성 제어 문제다.
Case 05. 로컬에서는 되던 Cookie 인증이 운영에서 흔들린다
상황
로컬에서는 로그인 유지가 잘 된다. 운영 배포 후에는 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가 인증 미들웨어에서 막히지 않는지 확인한다.
먼저 생각해보기
app.example.com과api.example.com은 같은 origin인가, 같은 site일 수는 있는가?- credentials 요청에서
Access-Control-Allow-Origin: *를 쓸 수 있는가?- 운영 HTTPS와 로컬 HTTP의 Cookie 동작 차이를 환경 표로 정리했는가?
찾아볼 문서
정리
Cookie 인증은 프론트 코드, CORS 응답, Cookie 속성, 배포 도메인이 같이 맞아야 한다. Postman이 아니라 브라우저의 Application 탭과 Network header가 실제 증거다. 운영에서만 깨진다면
Secure,SameSite, domain, HTTPS, preflight 응답을 환경별로 다시 본다.
가져갈 한 문장
Cookie 인증 디버깅은 credentials 한 줄이 아니라 origin, site, CORS, Cookie 속성의 조합을 보는 일이다.
Case 06. 서버 데이터를 useState에 복사했더니 화면마다 값이 달라진다
상황
프로젝트 목록을 여러 화면에서 사용한다. 한 화면에서 프로젝트를 보관했는데 다른 사이드바의 프로젝트 개수는 그대로다. 각 컴포넌트가 API 응답을 자기 useState에 복사해 들고 있다.
관찰할 증거
- 같은 서버 데이터가 몇 개의 local state에 복사되어 있는지 찾는다.
- 데이터의 source of truth가 서버인지 브라우저 UI인지 나눈다.
- mutation 후 어떤 query나 화면이 stale해지는지 본다.
- loading, error, empty, success 상태가 컴포넌트마다 반복 구현되어 있는지 확인한다.
먼저 생각해보기
- 이 값은 사용자가 브라우저에서 소유하는 상태인가, 서버가 소유하는 상태인가?
- cache, staleTime, refetch, invalidation이 필요한 데이터인가?
- 모달 open 여부와 프로젝트 목록은 같은 상태 관리 방식이어야 하는가?
찾아볼 문서
- [[01. 백엔드 개발자를 위한 프론트엔드 실전 판단#서버-상태를-usestate에-넣는-것과-tanstack-query를-쓰는-것은-뭐가-다를까|서버 상태를
useState에 넣는 것과 TanStack Query를 쓰는 것은 뭐가 다를까?]]- 전역 상태는 언제 필요한가?
- 01. API 연동과 서버 상태 상세
- 01. 컴포넌트 상태 렌더링 상세
정리
서버 상태는 프론트가 잠시 빌려 쓰는 복사본이다. 여러 화면에서 같은 데이터를 읽고 mutation으로 바꾼다면
useState복사보다 query cache와 invalidation 정책이 필요하다. 반대로 tab, modal, input draft처럼 브라우저가 소유하는 값은 가까운 local state가 더 단순하다.
가져갈 한 문장
상태 관리의 첫 질문은 “이 값의 source of truth가 서버인가, 브라우저인가”다.
Case 07. 보관 버튼을 눌렀는데 목록이 그대로다
상황
프로젝트 목록에서 보관 버튼을 눌렀다. API는 200을 반환했고 서버 데이터도 바뀌었다. 하지만 현재 목록에는 보관된 프로젝트가 계속 남아 있고, 새로고침해야만 사라진다.
관찰할 증거
- mutation 성공 후 어떤 query key를 invalidate하는지 확인한다.
- 목록 query key가 filter, status, page를 포함하는지 본다.
- 상세, 목록, 사용자 summary 중 같이 갱신되어야 할 데이터가 있는지 확인한다.
- optimistic update를 했다면 rollback 스냅샷이 있는지 본다.
먼저 생각해보기
- mutation 성공 후 stale해지는 query는 목록 하나뿐인가?
["projects"]prefix로 넓게 갱신할지, 특정 filter key만 갱신할지 정했는가?- 서버 응답만 성공하면 UI는 자동으로 최신이 되는가?
찾아볼 문서
- 01. API 연동과 서버 상태 상세
- [[01. 백엔드 개발자를 위한 프론트엔드 실전 판단#서버-상태를-usestate에-넣는-것과-tanstack-query를-쓰는-것은-뭐가-다를까|서버 상태를
useState에 넣는 것과 TanStack Query를 쓰는 것은 뭐가 다를까?]]- nullable API 응답 타입을 어떻게 표현해야 할까?
정리
mutation은 서버 상태를 바꾸지만, 프론트 cache를 자동으로 모두 최신화하지는 않는다. query key가 API parameter를 표현하고, mutation 성공 후 어떤 key를 stale 처리할지 정해야 한다. 넓은 prefix invalidation은 관련 목록을 함께 갱신하고, 좁은 key invalidation은 불필요한 refetch를 줄인다.
가져갈 한 문장
mutation을 만들 때는 API 호출보다 “성공 후 어떤 화면이 stale해지는가”를 같이 설계한다.
Case 08. 좋아요는 바로 바뀌는데 실패하면 화면이 거짓말을 한다
상황
사용자가 좋아요를 누르면 화면을 즉시 바꾸고 싶다. optimistic update를 적용했더니 UX는 빨라졌지만, 서버 실패 후에도 화면에는 좋아요가 유지된다. 결제나 재고 차감에도 같은 방식으로 적용할지 고민된다.
관찰할 증거
- 실패 시 이전 cache로 rollback할 수 있는 스냅샷이 있는지 확인한다.
- 실패 비용이 낮은 상호작용인지, 금액/권한/재고처럼 보수적으로 다뤄야 하는지 나눈다.
- mutation 실패 code와 충돌 처리 정책이 백엔드에 있는지 본다.
- 성공 후 서버 최신 상태를 재조회할 기준이 있는지 확인한다.
먼저 생각해보기
- 이 기능은 실패해도 쉽게 되돌릴 수 있는가?
- 실패했을 때 사용자가 보는 화면과 서버 상태가 달라져도 괜찮은가?
- optimistic update 후 최종 동기화는 어떻게 할 것인가?
찾아볼 문서
정리
optimistic update는 빠른 UX를 주지만 실패 시 복구할 수 있어야 한다. 좋아요나 북마크처럼 rollback이 쉬운 기능에는 잘 맞지만, 결제, 권한, 재고처럼 실패 비용이 큰 작업에는 보수적인 흐름이 안전하다. 서버 실패 코드와 최신 상태 재조회 기준을 백엔드와 맞춰야 한다.
가져갈 한 문장
낙관적 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를 나누는지 확인한다.
- 서버 내부 예외 문장이 사용자 화면에 노출되는지 본다.
먼저 생각해보기
- 이 실패에서 사용자가 할 수 있는 다음 행동은 무엇인가?
- 프론트가 상태별 UI를 만들 수 있을 만큼 error schema가 충분한가?
- 사용자 표시용 메시지와 개발자 디버깅 정보가 분리되어 있는가?
찾아볼 문서
정리
좋은 error schema는 예쁜 응답 포맷이 아니라 UI 행동을 결정하게 해주는 계약이다. field error는 input 아래에, 인증 만료는 로그인 흐름에, 권한 부족은 접근 불가 상태에 연결되어야 한다.
traceId는 사용자 신고와 서버 로그를 연결하는 실전 도구가 된다.
가져갈 한 문장
에러 응답은 “실패했다”가 아니라 “사용자가 다음에 무엇을 해야 하는가”를 알려줘야 한다.
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에 남아야 하는가, local state면 충분한가?
- 필터가 바뀌면 page를 어떻게 처리해야 하는가?
- 프론트 UI 방식이 page 기반인지 cursor 기반인지 API와 맞는가?
찾아볼 문서
- pagination, infinite scroll, search filter는 API와 어떻게 맞춰야 할까?
- 01. API 연동과 서버 상태 상세
- 01. URL Layout Navigation 상세
- [[02. 백엔드 개발자를 위한 프론트엔드 문법 함정과 디버깅#와-를-섞어-쓰면-어떤-버그가-생길까|
==와===를 섞어 쓰면 어떤 버그가 생길까?]]
정리
목록 상태는 URL, query key, API parameter가 함께 맞아야 안정적이다. 검색과 필터는 공유와 복원이 필요하므로 URL에 잘 맞고, query key에도 들어가야 cache가 섞이지 않는다. page/cursor와 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로 나가는지 확인한다.
먼저 생각해보기
- DOM input의
value는 기본적으로 어떤 타입인가?- 빈 문자열은 “값 없음”인가, 숫자 0인가?
- 값 변환은 onChange마다 할 것인가, 제출 경계에서 할 것인가?
찾아볼 문서
정리
number input도 브라우저 입력 경계에서는 문자열로 다뤄야 한다. 빈 문자열,
NaN, 0은 서로 다른 의미를 가질 수 있으므로 제출 전 변환과 schema 검증을 명확히 둔다. Zod를 쓴다면z.coerce.number()가 빈 문자열을 의도와 다르게 바꾸지 않는지 반드시 확인한다.
가져갈 한 문장
입력 중인 값은 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 name은 프론트 field name과 항상 같다고 믿어도 되는가?
- 매핑할 수 없는 에러는 어디에 보여줘야 하는가?
- field error를 toast 하나로만 보여주면 사용자는 무엇을 놓치는가?
찾아볼 문서
정리
서버 validation error는 UI와 API 계약이 직접 만나는 지점이다. field name 매핑이 가능해야 input 아래에 정확히 표시할 수 있고, 알 수 없는 field는 form-level error로 안전하게 처리해야 한다. 타입 단언은 검증이 아니므로 type guard나 매핑 테이블이 더 신뢰할 수 있다.
가져갈 한 문장
서버 field error는 toast가 아니라 사용자가 고칠 input으로 돌아가야 한다.
Case 13. 버튼을 한 번 눌렀는데 주문이 두 번 생성된다
상황
사용자는 결제 버튼을 한 번 눌렀다고 말한다. 로그를 보면 같은 주문 생성 API가 짧은 시간에 두 번 호출되었다. 프론트는 버튼 disable을 넣었지만, 네트워크 지연, 뒤로가기, 재시도, 새로고침까지 막지는 못했다.
관찰할 증거
- submit 중
isSubmitting이나 mutation pending 상태로 버튼이 비활성화되는지 본다. - 버튼 label이 처리 중 상태를 보여주는지 확인한다.
- 주문, 결제, 가입 같은 API에 idempotency key가 있는지 본다.
- 같은 요청이 재시도될 때 서버 응답 정책이 있는지 확인한다.
먼저 생각해보기
- 버튼 disable은 UX 방어인가, 데이터 무결성 방어인가?
- 중복 생성이 치명적인 API는 프론트만으로 막을 수 있는가?
- 공통 Button의 기본
type과 form submit 의도를 명시했는가?
찾아볼 문서
정리
submit pending 상태와 버튼 비활성화는 반드시 필요하지만, 중요한 생성 작업에서는 충분하지 않다. 프론트는 중복 클릭을 줄이고 사용자의 현재 상태를 보여주는 UX 방어를 맡고, 백엔드는 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,!가 있는지 검색한다.
먼저 생각해보기
- TypeScript 타입은 서버 JSON을 런타임에 검증하는가?
as User는 검증인가, 컴파일러에게 하는 주장인가?- 검증은 API 함수, hook, 컴포넌트 중 어디에서 하는 것이 자연스러운가?
찾아볼 문서
- [[02. 백엔드 개발자를 위한 프론트엔드 문법 함정과 디버깅#typescript의-any-unknown-as-non-null-assertion은-언제-위험할까|TypeScript의
any,unknown,as, non-null assertion은 언제 위험할까?]]- nullable API 응답 타입을 어떻게 표현해야 할까?
- 01. 비동기 타입 설계 상세
- OpenAPI만 있으면 충분한가?
정리
TypeScript는 컴파일 타임 도구이고, API 응답은 런타임 입력이다. 외부 입력은
unknown으로 받고 schema 검증이나 신뢰 가능한 타입 생성 파이프라인으로 좁히는 습관이 안전하다. nullable은 API 타입에 드러내고, view model에서 화면 기본값으로 좁히면 컴포넌트가 단순해진다.
가져갈 한 문장
as는 값을 안전하게 만드는 코드가 아니라, 이미 안전하다고 주장하는 코드다.
Case 15. 검색어를 바꿨는데 결과가 예전 그대로다
상황
검색 화면에서 keyword prop이 바뀐다. 하지만 effect dependency array가 비어 있어 첫 검색 결과만 계속 보인다. dependency를 추가하니 이번에는 요청이 너무 자주 반복되거나, 먼저 보낸 요청이 늦게 도착해 최신 결과를 덮어쓴다.
관찰할 증거
- effect 안에서 읽는 props와 state가 dependency에 들어 있는지 본다.
- keyword가 바뀔 때 이전 요청을 abort하거나 무시하는지 확인한다.
- 검색어 debounce와 서버 부하 정책을 정했는지 본다.
- 이 서버 상태를 직접 effect로 다룰지 TanStack Query로 옮길지 판단한다.
먼저 생각해보기
- effect는 “마운트 시 실행”인가, 외부 시스템과 동기화하는 도구인가?
- dependency lint를 끄면 어떤 오래된 값 문제가 생기는가?
- 검색 요청은 취소와 최신성 문제를 어떻게 다룰 것인가?
찾아볼 문서
- [[02. 백엔드 개발자를 위한 프론트엔드 문법 함정과 디버깅#useeffect-dependency-array-실수는-왜-자주-생길까|
useEffectdependency array 실수는 왜 자주 생길까?]]- 02. useEffect 이벤트 디버깅 상세
- 01. 비동기 타입 설계 상세
- 01. API 연동과 서버 상태 상세
정리
useEffect는 렌더링 값과 외부 시스템을 동기화한다. effect 안에서 읽는 값은 dependency에 포함해야 하고, API 요청은 실패 처리와 cleanup까지 함께 다뤄야 한다. 목록과 검색처럼 서버 상태 정책이 반복되면 TanStack Query가 더 단순한 선택이 될 수 있다.
가져갈 한 문장
검색 effect의 핵심은 dependency, 취소, 최신성, 서버 상태 도구의 경계를 같이 보는 것이다.
Case 16. 알림 숫자가 interval 안에서 멈춘다
상황
알림 숫자를 1초마다 증가시키는 interval을 만들었다. 그런데 숫자는 1에서 멈추거나, 오래된 count를 계속 기준으로 계산한다. console을 찍어보면 callback이 현재 state가 아니라 처음 렌더링 때의 값을 보고 있다.
관찰할 증거
- interval, timeout, subscription callback이 어떤 렌더링 시점의 값을 캡처했는지 본다.
- 이전 state로 다음 state를 만들면서 functional update를 쓰는지 확인한다.
- 최신 값을 읽어야 하는지, 이전 값에서 다음 값을 계산해야 하는지 나눈다.
- cleanup이 있는지 확인한다.
먼저 생각해보기
- React 함수 컴포넌트의 값은 렌더링마다 같은 값인가?
- callback은 생성 당시 값을 기억할 수 있는가?
- ref와 functional update는 각각 어떤 경우에 맞는가?
찾아볼 문서
- React stale closure는 왜 생길까?
- [[02. 백엔드 개발자를 위한 프론트엔드 문법 함정과 디버깅#setstate-직후-값이-바로-바뀌었다고-생각하면-왜-틀릴까|
setState직후 값이 바로 바뀌었다고 생각하면 왜 틀릴까?]]- 01. 컴포넌트 상태 렌더링 상세
정리
React state는 렌더링 시점의 snapshot이다. interval이나 subscription callback은 생성 당시 값을 캡처할 수 있으므로, 이전 state 기반 계산에는 functional update를 쓰는 편이 안전하다. 최신 값을 읽어야 하는 경우와 다음 값을 계산해야 하는 경우를 구분하면 stale closure 디버깅이 쉬워진다.
가져갈 한 문장
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 identity를 보존하는 기준인가?
- 정렬과 삭제가 있는 목록에서 index key가 어떤 상태 재사용을 만들 수 있는가?
- 서버 id가 아직 없는 임시 항목은 어떤 id를 써야 하는가?
찾아볼 문서
정리
React key는 DOM 재사용과 컴포넌트 state 보존의 기준이다. 정렬, 삽입, 삭제가 있는 목록에서 index key를 쓰면 다른 item의 상태가 이어붙는 논리 버그가 생길 수 있다. 서버 id가 없으면 임시 client id를 만들어 identity를 안정화한다.
가져갈 한 문장
목록 key는 “잘 렌더링되게 하는 값”이 아니라 “같은 item임을 증명하는 값”이다.
Case 18. 화면은 빈칸인데 에러는 없다
상황
사용자 프로필 화면의 제목이 빈칸이다. 콘솔 에러도 없고 API도 200이다. 코드를 보니 필수 데이터처럼 보이는 값마다 optional chaining이 붙어 있어 계약 위반이 조용히 undefined로 숨겨지고 있다.
관찰할 증거
- UI 깊은 곳에
?.가 반복되는지 검색한다. - query loading, error, empty 경계에서 데이터를 좁힌 뒤 컴포넌트에 넘기는지 본다.
- nullable과 optional이 API 계약에서 구분되어 있는지 확인한다.
- view model 변환 지점이 있는지 본다.
먼저 생각해보기
- 이 값은 진짜 optional인가, API 계약상 필수인가?
- 필수 데이터가 빠졌을 때 빈칸이 맞는가, 에러가 맞는가?
- 화면 컴포넌트가 방어를 반복하는 대신 데이터 경계에서 좁힐 수 있는가?
찾아볼 문서
정리
optional chaining은 안전한 회피 도구가 아니라 nullish 접근을 조용히 멈추는 문법이다. 필수 데이터에는 로딩, 에러, empty 경계가 필요하고, nullable 값은 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를 유발하는지 본다.
먼저 생각해보기
- CORS는 서버 간 통신 제한인가, 브라우저가 응답 노출을 제한하는 정책인가?
- Postman에서 성공한 사실이 브라우저 성공을 증명하는가?
- preflight 요청이 인증 미들웨어에 막히고 있지는 않은가?
찾아볼 문서
정리
CORS 실패는 프론트 코드 에러처럼 보이지만, 해결 지점은 대부분 서버 응답 헤더와 preflight 처리다. 브라우저는 origin과 credentials 정책을 검사하고, 허용되지 않은 응답은 JavaScript에 넘기지 않는다. Network의 preflight와 response header가 가장 좋은 증거다.
가져갈 한 문장
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에 남아도 되는 정보인가?
- path param과 query param의 역할을 구분했는가?
찾아볼 문서
- URL query param에 상태를 넣어도 되는가?
- 01. URL Layout Navigation 상세
- 01. 브라우저 HTTP DOM 상세
- [[02. 백엔드 개발자를 위한 프론트엔드 문법 함정과 디버깅#와-를-섞어-쓰면-어떤-버그가-생길까|
==와===를 섞어 쓰면 어떤 버그가 생길까?]]
정리
URL은 상태 저장소이면서 사용자 인터페이스다. 검색어, 필터, 정렬, 페이지처럼 공유와 복원이 필요한 상태에는 잘 맞지만, token, 비밀번호, 임시 입력값에는 맞지 않는다. URL에 들어간 값은 사용자의 화면 밖에서도 오래 남을 수 있다.
가져갈 한 문장
URL에 넣는 값은 “공유되어도 되는 상태”인지 먼저 확인한다.
Case 21. 뒤로가기를 눌렀는데 검색 조건이 복원되지 않는다
상황
목록 화면에서 검색어와 상태 필터를 바꿨다. 뒤로가기를 누르거나 새로고침하면 검색 조건이 사라지고 기본 목록으로 돌아간다. 사용자는 방금 보던 목록을 다시 찾기 어렵다.
관찰할 증거
- search, filter, sort, page가 URL query param에 들어 있는지 본다.
- query key가 URL 상태와 같은 값을 사용하는지 확인한다.
- query param 변경 시 page reset과 scroll reset이 필요한지 본다.
- 모달이나 임시 form 값처럼 URL에 넣지 않아야 할 상태와 구분한다.
먼저 생각해보기
- 이 상태는 뒤로가기와 새로고침에서 복원되어야 하는가?
- URL query param과 API parameter 이름이 맞는가?
- query param 값이 string이라는 사실을 처리했는가?
찾아볼 문서
정리
목록 조건은 URL에 둘 때 사용자가 다시 돌아오고 공유할 수 있다. 이 URL 상태는 query key와 API parameter에도 반영되어야 cache가 정확히 나뉜다. 단, URL은 오래 남는 표면이므로 민감하거나 휘발적인 상태는 local state로 둔다.
가져갈 한 문장
뒤로가기와 새로고침에서 살아야 하는 목록 상태는 URL과 query key에 같이 들어간다.
Case 22. 긴 제목 하나가 카드와 테이블 레이아웃을 밀어낸다
상황
프로젝트 제목이 길게 들어오자 카드가 부모 너비를 밀어내고, table은 모바일에서 화면 밖으로 크게 삐져나간다. CSS에 overflow: hidden과 text-overflow: ellipsis를 넣었지만 말줄임이 동작하지 않는다.
관찰할 증거
- flex/grid child 또는 중간 컨테이너에
min-width: 0이 있는지 확인한다. - 긴 URL, code block, table 같은 intrinsic size가 큰 자식이 있는지 본다.
- DevTools Layout panel에서 실제 box size와 overflow를 확인한다.
- 백엔드 fixture에 긴 제목, 빈 값, 긴 URL 같은 깨지는 데이터가 있는지 본다.
먼저 생각해보기
- ellipsis가 안 될 때 먼저 의심할 속성은 무엇인가?
- table과 긴 텍스트는 같은 overflow 전략으로 처리할 수 있는가?
- 모바일 목록에는 desktop table과 같은 필드가 모두 필요한가?
찾아볼 문서
정리
flex/grid 레이아웃에서 긴 콘텐츠가 줄어들지 않으면
min-width: 0과 intrinsic size를 먼저 확인한다. 반응형은 단순 column 전환이 아니라 작은 화면에서 어떤 정보를 유지할지 정하는 제품 결정이다. 백엔드 fixture에도 화면을 깨뜨리는 긴 값과 빈 값을 넣어야 한다.
가져갈 한 문장
긴 텍스트가 레이아웃을 밀면 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으로 관리하는지 본다.
먼저 생각해보기
z-index는 전역 숫자 경쟁인가, stacking context 안의 경쟁인가?- rounded corner clipping과 overlay는 같은 컨테이너에 있어도 되는가?
- dropdown, tooltip, modal은 언제 portal이 필요한가?
찾아볼 문서
- [[02. 백엔드 개발자를 위한 프론트엔드 문법 함정과 디버깅#overflow는-단순히-스크롤을-만드는-속성일까|
overflow는 단순히 스크롤을 만드는 속성일까?]]- [[02. 백엔드 개발자를 위한 프론트엔드 문법 함정과 디버깅#z-index를-올렸는데-왜-위로-안-올라올까|
z-index를 올렸는데 왜 위로 안 올라올까?]]- 01. CSS Layout Responsive 상세
정리
overlay 문제는
z-index숫자만 올려서 해결되지 않는다. 부모 overflow는 자식을 잘라내고, stacking context는 z-index 경쟁 범위를 제한한다. dropdown과 modal은 portal, focus, layer scale을 함께 설계하는 것이 안전하다.
가져갈 한 문장
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 상태를 확인한다.
먼저 생각해보기
- sticky가 붙을 scroll container는 누구인가?
- 모바일 브라우저의 보이는 높이와 CSS
100vh는 항상 같은가?- fixed bottom UI는 safe area와 keyboard 상황을 고려했는가?
찾아볼 문서
- [[02. 백엔드 개발자를 위한 프론트엔드 문법 함정과 디버깅#position-sticky가-안-붙는-이유는-무엇일까|
position: sticky가 안 붙는 이유는 무엇일까?]]- [[02. 백엔드 개발자를 위한 프론트엔드 문법 함정과 디버깅#모바일-viewport와-100vh는-왜-함정일까|모바일 viewport와
100vh는 왜 함정일까?]]- 01. CSS Layout Responsive 상세
정리
sticky와 모바일 viewport는 부모 overflow, scroll container, 브라우저 UI 높이의 영향을 받는다.
100vh는 모바일 주소창과 키보드 상황에서 기대와 다를 수 있으므로100dvh,min-height, safe area를 함께 검토한다. 실제 기기에서 확인하는 과정이 중요하다.
가져갈 한 문장
모바일 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 정책이 있는지 확인한다.
먼저 생각해보기
- 디자인 시스템은 색상표인가, 상태 표현의 일관성인가?
- 공통 Button의 loading과 disabled는 어떤 버그를 줄이는가?
- enum 값이 추가되면 프론트 UI가 컴파일 단계에서 드러나는가?
찾아볼 문서
정리
작은 프로젝트에서도 공통 컴포넌트의 상태를 정하면 완성도가 크게 오른다. 디자인 시스템은 거창한 문서가 아니라 같은 의미의 UI를 같은 방식으로 표현하는 최소 규칙이다. 서버 enum과 UI variant 매핑을 명시하면 백엔드 변경이 화면 깨짐으로 늦게 발견되는 일을 줄일 수 있다.
가져갈 한 문장
좋은 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이 있는지 확인한다.
먼저 생각해보기
- 성능 병목은 프론트 렌더링만의 문제인가?
- 목록 API와 이미지 제공 방식이 화면 성능에 어떤 영향을 주는가?
- 최적화 전에 무엇을 측정해야 하는가?
찾아볼 문서
정리
성능 문제는 프론트 코드만의 책임으로 좁히면 절반을 놓친다. API 응답 크기, pagination, image thumbnail, cache header는 화면 속도에 직접 영향을 준다. 측정 없이 memoization부터 넣기보다 Network, Performance, Lighthouse로 병목을 나눈다.
가져갈 한 문장
느린 화면은 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은 접근성만을 위한 것인가, 테스트 안정성과도 연결되는가?
aria-*를 붙이기 전에 기본 HTML 요소로 해결할 수 있는가?- 사용자가 keyboard만으로 핵심 흐름을 완료할 수 있는가?
찾아볼 문서
정리
접근성은 나중에 붙이는 장식이 아니라 UI 구조의 기본이다. semantic HTML을 쓰면 keyboard interaction과 보조 기술 이름이 자연스럽게 따라오고, Testing Library의 role/label 기반 테스트도 안정적이 된다.
div onClick은 간단해 보이지만 직접 구현해야 할 기본 동작이 많다.
가져갈 한 문장
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 값은 런타임 서버 env처럼 바뀌는가?
- 브라우저 bundle에 들어간 env 값은 secret인가?
- 오래된 HTML과 hashed asset cache는 어떻게 다르게 다뤄야 하는가?
찾아볼 문서
정리
Vite 같은 정적 빌드에서 env는 build 시점에 bundle로 들어간다. 배포 후 서버 env만 바꾸어도 이미 만들어진 JS는 바뀌지 않으므로 다시 build/deploy해야 한다. 운영 디버깅에는 release version, source map, HTML cache, asset cache, API URL 조합 규칙, 서버 로그와 연결할 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와 다르면 테스트는 어떤 착각을 만드는가?
- unit, component, e2e는 각각 무엇을 검증해야 하는가?
- API 문서에는 성공 응답뿐 아니라 어떤 실패와 경계값이 필요할까?
찾아볼 문서
정리
테스트는 실제 계약과 가까울수록 가치가 있다. MSW는 프론트 테스트를 안정화하지만, mock이 실제 API와 달라지면 통과하는 테스트가 위험한 신호가 될 수 있다. 성공 응답보다 validation error, 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는 각각 어떤 최소 흐름만 둘 것인가?
찾아볼 문서
정리
이 미션은 완성된 정답을 찾는 문제가 아니다. 각 선택마다 “왜 이 선택을 했는가”, “백엔드와 무엇을 맞춰야 하는가”, “어떤 위험 신호를 먼저 볼 것인가”를 남기는 과정이다. 작은 앱 하나에 인증, API 계약, form, routing, UI 상태, 배포, 테스트가 모두 들어가면
00~13의 지식이 실제 개발 순서로 다시 연결된다.
가져갈 한 문장
좋은 개인 프로젝트는 기능 목록이 아니라 선택 기준과 위험 신호가 코드에 남아 있는 프로젝트다.