Devin.KR

API 계약과 출력 안전성

60분 안팎

학습 목표

잘못된 응답과 신뢰 경계를 확인합니다.

개념

응답을 화면 상태로 바꾸기 전에 경계를 확인합니다

HTTP 200은 서버가 응답했다는 정보이며 행사 목록의 필드가 사용 가능한지는 별도로 확인합니다. id가 빠진 행사를 카드로 만들면 상세 경로를 만들 수 없고, 같은 id의 두 행사는 카드 캐시의 정체성을 혼동시킵니다. 이번 레슨은 외부 JSON을 앱 데이터로 받아들이는 경계를 명시하고, 계약 오류를 빈 결과로 숨기지 않으며 제목은 텍스트로 보여 주는 검사를 완성합니다.

기존 api.mjs의 request는 네트워크 응답을 받고 상태·Content-Type·JSON 해석을 확인한 뒤 validate를 호출합니다. validate는 목록이면 배열을, 상세면 단일 레코드를 배열로 감싸 검사합니다. 새 safety.mjs의 validRows는 필드와 목록 내 유일성을 판단합니다. 분리한 검사는 화면이나 fetch 없이도 호출할 수 있지만 앱이 실제 validate를 거쳐야 효과가 있습니다. export만 만들고 연결을 빠뜨리지 않습니다.

이번 계약의 id는 0보다 큰 안전한 정수입니다. 문자열 2와 숫자 2는 다르게 취급합니다. 서버가 문자열로 보내도 Number로 자동 보정하지 않고 계약 오류로 돌려보냅니다. id가 없으면 임의 번호를 붙이지 않습니다. 상세 링크와 카드 재사용의 기준이므로 입력 순서에서 만든 번호가 실제 행사 식별자인 것처럼 보이는 결과를 피해야 합니다.

배열 안에서 같은 id가 반복되면 거절합니다. Set에 이미 있는지 검사한 뒤 정상 id를 추가하면 순서와 관계없이 중복을 찾을 수 있습니다. 제목이 서로 다르더라도 같은 id이면 중복입니다. 이미 있던 원소를 조용히 지우면 API 변경을 숨길 수 있으므로 이 경계에서는 오류로 처리합니다. 별도의 중복 정리 정책이 필요하다면 서버 계약과 테스트를 함께 합의합니다.

이름은 문자열이어야 하며 trim한 값이 빈 문자열이면 실패합니다. 반환할 때 원래 이름을 자동 수정하지는 않습니다. 검증과 정규화는 다른 책임이기 때문입니다. 지역은 seoul 또는 busan이며 date는 YYYY-MM-DD 형식입니다. 정규식만으로 2월 31일의 실제 존재를 확인하지는 않습니다. 이 실습의 계약 범위를 날짜 형식 검증이라고 명확히 적고 행사 진위나 날짜 유효성까지 보장한다고 주장하지 않습니다.

빈 배열은 성공적인 응답이며 검색 결과가 없는 상태입니다. 객체 하나가 목록 위치에 온 경우는 계약 오류이고 상세 객체는 detail 옵션이 있을 때만 허용합니다. null은 typeof 결과가 object이므로 객체 검사만으로 통과시키면 안 됩니다. 필드에 접근하기 전에 레코드 존재를 검사합니다. 같은 입력이라도 목록과 상세의 경로를 구분하는 이유를 테스트에서 읽을 수 있어야 합니다.

load는 먼저 loading과 빈 rows를 발행하고 응답을 처리한 뒤 success·empty·error 상태를 보냅니다. 잘못된 응답을 받아도 과거 목록을 그대로 보여 주면 사용자는 현재 조건의 결과로 오해합니다. safety-test.mjs는 id 누락 응답에서 상태 순서가 loading 다음 error인지, reason이 contract인지, rows가 비었는지 확인합니다. 오류를 0건으로 바꿔 테스트를 통과시키지 않습니다.

API 오류와 화면 메시지는 같은 문자열일 필요가 없습니다. ApiError의 kind는 계약 분류를 전달하고 StatusPanel은 사용자가 이해할 수 있는 안내를 선택합니다. contract는 응답 형식 안내로, network는 연결 확인 안내로 나타납니다. 내부 스택과 원문 응답을 그대로 화면에 출력하지 않습니다. 재시도는 같은 입력으로 다시 요청할 기회를 주지만 잘못된 서버 계약을 항상 해결하는 것은 아닙니다.

출력 안전성은 데이터가 들어가는 위치에 따라 판단합니다. 행사 이름은 일반 h2와 span의 textContent에 넣습니다. HTML처럼 보이는 문자열도 제목 텍스트로 보여야 합니다. 이름을 innerHTML에 붙이면 문자열을 문서로 해석하는 경계를 넘습니다. API 응답이 개발자가 만든 mock에서 시작했어도 향후 외부 데이터가 된다는 가정을 검사에 넣으면 우연한 안전에 의존하지 않습니다.

textContent를 쓰면 어떤 요소에서도 안전하다는 설명은 틀립니다. script 요소의 텍스트는 실행 코드가 될 수 있습니다. 이번 EventCard는 일반 제목과 링크 라벨을 만들고 공격 형태의 제목을 거기에 넣는 좁은 계약입니다. 텍스트 삽입과 URL 설정도 다릅니다. 제목을 안전하게 넣었다고 외부 링크 주소나 이벤트 속성까지 안전해지지는 않습니다. 사용할 문맥과 허용 값을 따로 검토합니다.

현재 링크는 앱 내부 results.html과 검증된 숫자 id로 만들고 이미지 주소는 고정된 assets/event.svg입니다. API의 임의 URL을 그대로 href나 src에 쓰지 않습니다. 향후 외부 링크를 지원하면 허용 scheme과 출처 정책을 정해야 합니다. API 데이터는 인증이나 권한 판단의 근거도 아닙니다. 클라이언트 검증을 추가해도 서버 권한 검사가 필요 없어지는 것으로 설명하지 않습니다.

모델 테스트의 doc은 createElement와 append를 제공하며 innerHTML에 쓰면 즉시 예외를 냅니다. 이름에 이미지 태그처럼 생긴 문자열과 앰퍼샌드를 넣어 h2와 span의 textContent가 원문인지 검사합니다. 이 검사는 개발자가 삽입 방식을 바꾸면 실패하지만 실제 브라우저의 스크립트 실행이나 CSS·포커스를 재현하지는 않습니다. 어떤 위험을 잡고 어떤 위험을 남기는지 결과 문서에 함께 씁니다.

실제 화면 확인용 Playwright 검사에서는 악성 형태 제목을 route로 제공하고 목록 라벨과 상세 제목이 그대로 보이는지 확인합니다. 예상치 못한 이미지 요소가 생기지 않는지, window 표식이 바뀌지 않는지도 관찰합니다. 외부 사이트에 공격 문자열을 보내지 않습니다. 제공된 로컬 mock에만 테스트 데이터를 넣고 테스트 로그와 캡처를 연결합니다. 실행하지 않은 검사에는 실제 출력이나 성공 표식을 넣지 않습니다.

오류 읽기의 출발점은 기대한 실패 위치입니다. AssertionError가 중복 id 검사에서 나오면 Set 구성과 추가 순서를 확인합니다. Cannot read properties of null이 나오면 필드 읽기 전 존재 검사가 빠졌는지 봅니다. contract 오류가 정상 빈 목록에서도 나오면 빈 배열을 금지해 버린 조건이 있는지 확인합니다. assertion의 expected와 actual을 보고 입력 계약부터 좁히면 화면 전체를 무작정 바꿀 필요가 줄어듭니다.

확인 데이터에는 정상 목록·빈 목록·상세·누락·0·문자열 id·공백 이름·잘못된 지역·날짜 형식·중복을 포함합니다. 정상 테스트도 함께 유지해야 과도하게 모든 응답을 거절한 구현을 막을 수 있습니다. solution에서 validRows를 완성하고 기존 API·요청 순서 회귀가 그대로 통과하는지 확인합니다. 늦은 응답 폐기와 화면 종료 정책은 다른 계약이므로 안전성 보완을 이유로 제거하지 않습니다.

완료 기준은 잘못된 레코드를 오류로 구분하고 유효한 빈 배열을 빈 결과로 표현하며 공격 형태 이름을 일반 텍스트로 다루는 것입니다. 아직 실제 화면을 실행하지 않았다면 Node 모델의 보장 범위까지만 설명합니다. 더 읽기에서는 fetch와 JSON 처리 흐름을 다시 확인하고 이 과제의 출력 문맥과 id 유일성 판단은 자신의 코드·반례를 이용해 설명합니다.

삽입 문맥 확인: MDN textContent 문서에서 일반 텍스트와 HTML 해석의 차이를 확인합니다.

따라하기

중복 식별자의 반례를 확인합니다

독립 예제에서 두 행의 id는 같으므로 유일성 결과를 확인합니다.

const rows=[{id:2},{id:2}];const ids=new Set(rows.map(e=>e.id));console.log(ids.size===rows.length);

실행 결과

false

경계 테스트를 먼저 읽습니다

safety-test.mjs의 정상·빈 목록·상세와 누락·0·문자열 id·공백·중복 반례를 읽습니다. npm test로 starter의 assertion 실패를 확인합니다.

검사를 앱에 연결합니다

assets/safety.mjs의 validRows를 구현합니다. api.mjs가 validate에서 호출하는 연결을 유지하고 npm test에서 기존 요청 순서 회귀까지 확인합니다.

실제 출력 문맥을 검토합니다

e2e/safety.spec.cjs의 route fixture로 HTML 형태 제목·누락 id를 제공하고 npm run test:built로 확인합니다. 일반 제목이 텍스트로 보이며 불필요한 이미지가 없고 오류가 empty와 구분되는지 관찰합니다.

확인 문제

실습

assets/safety.mjs를 구현합니다. 누락·0·문자열 id·공백 이름·중복·지역·날짜 형식을 거절하고 정상 목록·빈 목록·상세는 허용합니다. EventCard의 일반 요소 textContent 삽입과 기존 회귀를 보존합니다.

시작 코드·테스트 내려받기

실행 명령

npm test

기대 결과

starter는 새 계약 assertion 실패, solution은 기존 회귀와 새 검사 실패 0

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • 로딩·오류·빈 결과 화면을 나눈 이유를 설명합니다.