
타임시트의 검증은 다섯 층으로 나뉜다. 계산 로직은 단위 테스트로, API 는 실행 중인 서버에 요청을 보내는 e2e 로 확인했다. 운영 환경은 같은 e2e 를 운영 주소에 대고 돌렸다. 화면은 브라우저에서 크기를 쟀고 속도는 스크립트와 브라우저 기록으로 쟀다. 테스트 프레임워크는 따로 설치하지 않았다. Node.js 에 들어 있는 node:test 하나로 웹과 서버 테스트를 모두 돌린다.
이 글은 층마다 무엇을 어떻게 돌렸는지, 돌리다 막힌 곳은 어디였는지, 그리고 끝내 확인하지 못한 것이 무엇인지를 정리한다.
| 층 | 도구 | 무엇을 보나 | 마지막 결과 |
|---|---|---|---|
| 웹 로직 단위 테스트 | node --test | 급여·지오펜스·달력·오늘 상태·시간표 계산 | 54/54 |
| 서버 e2e (로컬) | tsx --test | 로그인·매장·초대·요청·로고·시간표 API | 17/17 (+ 세션 상한 S17·S17b 2/2) |
| 운영 e2e | 같은 e2e 파일, 운영 주소 | 운영 환경(Vercel·Neon)에서의 동작 | 시나리오별 기록 |
| 화면 | 운영 가이드 + 390·320px iframe | 모바일 폭 넘침·높이·줄바꿈 | 측정값 기록 |
| 성능 | perf-boot.mts, Resource Timing | 응답 시간, 데이터 도착 시점, CLS | docs/verify/performance.md |
표 1. 타임시트의 검증 다섯 층
1. 웹 로직 단위 테스트 — node --test
1-1. 계산은 lib/ 로 빼고 화면과 따로 잰다
급여, 50m 지오펜스 알림, 달력 칸, 오늘 근무 상태, 근무 시간표 계산은 모두 web/src/lib/*.ts 에 순수 함수로 있다. 순수 함수는 같은 입력에 늘 같은 값을 돌려주고 화면이나 네트워크를 건드리지 않는 함수다. 그래서 브라우저 없이 Node 에서 바로 돌릴 수 있다.
"test": "node --test src/lib/*.test.mjs"
테스트 파일은 계산 파일 옆에 *.test.mjs 로 둔다. 테스트 이름 앞에는 PRD 의 수용 기준 번호(P-1, G-4, TD-6 …)를 붙여서 어느 요구를 확인하는지 바로 보이게 했다.
| 파일 | 테스트 수 | 확인하는 것 (PRD) |
|---|---|---|
pay.test.mjs | 16 | 기본급·주휴·연장 가산·5인 미만·휴가·대타 (02·03·09) |
calendar.test.mjs | 14 | 달력 칸·주 시작·기록 붙는 날짜 (10) |
today.test.mjs | 9 | 오늘 근무 상태·퇴근 뒤 버튼 (12) |
geo.test.mjs | 5 | 거리 계산, 첫 알림·60초 재알림 (04) |
inviteLink.test.mjs | 4 | 초대 가입 링크·문자 주소 (06) |
schedule.test.mjs | 4 | 요일·시각에서 주 시간 계산 (13) |
format.test.mjs | 2 | 시계 HH:MM:SS 형식 |
| 합계 | 54 |
표 2. 웹 단위 테스트 파일별 개수
이 글을 쓰면서 web/ 에서 node --test src/lib/*.test.mjs 를 다시 돌렸다. tests 54, pass 54, fail 0 이었다.
급여 테스트 하나는 이렇게 생겼다. 2026년 최저임금 10,320원으로 주 20시간·5일을 다 채우면 기본급과 주휴수당이 얼마인지 원 단위까지 맞춰 본다.
// docs/prd/02-pay.md P-1~P-4, docs/prd/03-overtime.md O-1~O-3
import { test } from "node:test";
import assert from "node:assert/strict";
import { computeWeek, computeWeeks, computeMonth, startOfWeek } from "./pay.ts";
const H = 3_600_000;
const monday = new Date(2026, 8, 21); // 2026-09-21 (월)
const at = (dayOffset, hour) => new Date(2026, 8, 21 + dayOffset, hour).getTime();
const shift = (dayOffset, from, hours) => ({ id: `${dayOffset}-${from}`, start: at(dayOffset, from), end: at(dayOffset, from) + hours * H });
const base = { hourlyWage: 10320, weeklyHours: 20, workDaysPerWeek: 5, fivePlus: true };
test("P-1 주 20시간·5일 개근 → 기본급 206,400 + 주휴 41,280", () => {
const w = computeWeek(monday, [0, 1, 2, 3, 4].map((d) => shift(d, 10, 4)), base);
assert.equal(w.basePay, 206400);
assert.equal(w.holidayPay, 41280);
assert.equal(w.overtimePay, 0);
assert.equal(w.total, 247680);
});
테스트 파일이 .mjs 인데 ./pay.ts 를 바로 import 한다. 이 PC 의 Node 는 24.19.0 이고 이 버전의 Node 는 .ts 파일의 타입 표기를 걷어내고 바로 실행한다. 그래서 빌드 단계 없이 TypeScript 소스를 테스트할 수 있다.
1-2. 확장자 없는 import — registerHooks
오늘 근무 대시보드를 만들 때 이 방식에서 막혔다.
Cannot find module .../src/lib/pay
today.ts 는 앱 코드 규칙대로 import … from "./pay" 처럼 확장자 없이 가져온다. Next.js 번들러는 .ts 를 붙여 찾아 주지만 Node 는 확장자를 추측하지 않는다. 테스트 파일이 ./today.ts 를 불러오는 것은 되는데, 그 안의 "./pay" 에서 멈춘 것이다.
앱 코드를 테스트에 맞춰 고치지 않고 테스트 쪽에서 Node 의 모듈 찾기에 훅을 걸었다. registerHooks 는 Node 가 import 경로를 해석하는 과정에 끼어드는 함수다. 먼저 원래대로 찾아보고 실패하면서 확장자가 없는 상대 경로면 .ts 를 붙여 다시 찾는다. 같은 문제를 먼저 겪은 calendar.test.mjs 의 방법을 그대로 가져왔다.
import { registerHooks } from "node:module";
import { test } from "node:test";
import assert from "node:assert/strict";
// today.ts 는 "./pay" 를 확장자 없이 import 한다 — calendar.test.mjs 와 같은 방법으로 .ts 를 붙여 찾는다
registerHooks({
resolve(specifier, context, next) {
try {
return next(specifier, context);
} catch (e) {
if (specifier.startsWith(".") && !/\.[cm]?[jt]s$/.test(specifier)) return next(`${specifier}.ts`, context);
throw e;
}
},
});
const { todayShifts, todayState, toMinute } = await import("./today.ts");
마지막 줄이 정적 import 가 아니라 await import() 인 이유가 있다. 정적 import 는 파일의 다른 코드보다 먼저 해석되므로, 훅을 등록하기 전에 ./pay 를 찾다가 실패한다. 동적 import 로 바꿔야 훅이 걸린 뒤에 불러온다.
1-3. 기능이 늘 때마다 같이 늘었다
첫 요청에서 15개로 시작한 테스트가 하루 동안 54개가 됐다. 새 계산이 생길 때마다 그 계산의 수용 기준을 테스트로 먼저 옮겼다.
| 시점 | 개수 | 더해진 것 |
|---|---|---|
| 요청 1 (급여·알림) | 15 | P·O·G 시나리오 |
| 요청 5 (휴가·대타) | 21 | L-1~L-4, S-1 |
| 요청 8 (근무 달력) | 35 | 달력 14 |
| 요청 13 (초대 문자) | 39 | 초대 링크 4 |
| 요청 20 (시계 한 줄) | 41 | clockText 2 |
| 요청 19 (오늘 근무) | 49 | 오늘 상태 8 |
| 요청 22 (다시 출근 없음) | 50 | TD-6 |
| 요청 24 (근무 시간표) | 54 | 시간표 4 |
표 3. 웹 단위 테스트가 늘어난 순서 (히스토리 [검증] 줄)
2. 서버 e2e — 실행 중인 API 에 요청을 보낸다
2-1. 테스트가 브라우저 흉내를 낸다
서버 테스트는 함수를 직접 부르지 않는다. 실제로 떠 있는 API 에 HTTP 요청을 보내고 응답을 확인한다. 이렇게 처음부터 끝까지 실제 경로로 확인하는 테스트를 e2e(end-to-end) 테스트라고 한다. 파일은 server/test/e2e.test.ts 하나이고 tsx --test 로 돈다. tsx 는 TypeScript 파일을 바로 실행하는 도구다.
"test:e2e": "tsx --test test/*.test.ts"
API 가 먼저 떠 있어야 한다. 저장소 문서(README.md, CLAUDE.md, docs/verify/auth.md)에는 모두 "pnpm dev 로 서버를 띄운 뒤 pnpm test:e2e" 로 적혀 있다.
로그인이 쿠키로 되기 때문에, 테스트 안에 쿠키를 저장하고 CSRF 토큰을 싣는 작은 클라이언트를 뒀다. 브라우저가 하는 일을 흉내 내는 것이다.
/** 브라우저 역할: 쿠키 저장소 + CSRF 토큰. 테스트마다 IP 를 달리해 로그인 제한이 섞이지 않게 한다. */
class Client {
jar = new Map<string, string>();
csrf: string | null = null;
ip = `10.0.0.${ipSeq++}`;
/** GET 이 아니면 기본으로 CSRF 헤더를 싣는다 (브라우저 FE 와 같다). csrf: false 로 뺄 수 있다. */
async req(method: string, path: string, o: { body?: unknown; csrf?: boolean; auth?: boolean; headers?: Record<string, string> } = {}) {
const wantCsrf = o.csrf ?? method !== "GET";
const csrf = wantCsrf ? await this.ensureCsrf() : null; // 쿠키 헤더를 만들기 전에 csrf 쿠키를 받아 둔다
const headers: Record<string, string> = { Origin: ORIGIN, "X-Forwarded-For": this.ip };
if (this.jar.size) headers.Cookie = [...this.jar].map(([k, v]) => `${k}=${v}`).join("; ");
// … 요청을 보내고 Set-Cookie 를 jar 에 반영 …
}
}
테스트 계정은 매번 ${randomUUID()}@test.dev 로 새로 만든다. 식별할 수 있는 도메인을 써야 운영에서 돌린 뒤 테스트 데이터만 골라 지울 수 있다(3절).
2-2. 처음 돌렸을 때 10개 중 8개가 403
처음 e2e 를 돌렸을 때 10개 중 8개가 403 으로 실패했다. 서버의 CSRF 검사가 거부한 것이다.
서버는 옳았다. 테스트 클라이언트가 요청 헤더의 Cookie 를 먼저 만들고 그다음에 CSRF 토큰을 받아 왔다. CSRF 토큰을 받는 요청이 csrf_token 쿠키를 새로 주는데, 이미 만들어 둔 Cookie 헤더에는 그 쿠키가 없었다. 서버는 헤더의 토큰과 쿠키의 토큰이 맞는지 보므로 403 을 돌려줬다.
토큰을 먼저 받고 쿠키 헤더를 나중에 만들도록 순서를 바꿨다. 위 코드의 "쿠키 헤더를 만들기 전에 csrf 쿠키를 받아 둔다" 주석이 그 자리다. 10/10 이 됐고 시나리오를 하나 더해 11/11 이 됐다.
2-3. 시나리오
| 묶음 | 시나리오 | 무엇 |
|---|---|---|
| 인증 | S1·S2, S3, S4, S5·S6, X-1, X-2, S7·S8·S9, S23, S10~S13, S14, S18, S19 | 가입, 쿠키 속성, 틀린 비밀번호 응답 시간 차, 변조 토큰, CSRF, 리프레시 회전과 재사용 탐지, 로그아웃, CORS, 로그인 횟수 제한 |
| 매장·초대 | I-1~I-8 | 초대 코드 등록·재사용·만료·취소, 권한, 출퇴근, 대시보드 |
| 요청 | C-1~C-7, L·S·M | 기록 수정 요청과 승인, 휴가·대타 |
| 로고 | LG-1~LG-7 | 형식 검사, SVG 안전, 크기, 권한 |
| 시간표 | WS-1~WS-6 | 시간표 저장·계산, 날짜별 변경 |
표 4. 서버 e2e 시나리오 (test() 17개)
test() 호출은 파일에 19개 있다. 그중 S17·S17b 둘은 세션 상한을 5초로 줄여 띄운 서버가 따로 필요해서 S17=1 환경변수가 있을 때만 돈다. 30일을 기다릴 수 없으니 서버 설정을 바꿔 5초 뒤 만료되는지, 3초마다 쓰면 기한이 밀리는지를 본다.
# 서버를 SESSION_MAX_AGE_SEC=5 PORT=4201 로 띄운 뒤
S17=1 API=http://localhost:4201 pnpm test:e2e
로컬 결과는 11/11 → 15/15 → 17/17 로 늘었고 S17·S17b 는 2/2 였다.
3. 운영 e2e — 같은 파일을 운영에 대고
같은 e2e 파일을 운영에 대고 돌렸다. API·WEB_ORIGINS 를 웹 운영 주소로, DATABASE_URL 을 운영 DB 로 준다. 요청은 웹 주소로 들어가 rewrites 를 거쳐 API 에 닿는다. 로컬에서는 보이지 않던 것이 세 가지 나왔다.
| 무엇 | 로컬 | 운영 | 원인 |
|---|---|---|---|
alg:none 토큰(S5·S6) | 401 | 403 | Vercel 방화벽이 앱에 닿기 전에 막는다(x-vercel-mitigated: deny) |
| 로그인 횟수 제한 | 테스트마다 IP 가 달라 섞이지 않음 | 뒤쪽 I-1 이 429 | Vercel 이 X-Forwarded-For 를 덮어써 모든 요청이 실제 IP 하나로 잡힘 |
| 틀린 비밀번호 응답 시간(S4) | 36.8 / 36.3ms | 316.0 / 271.3ms | 네트워크 편차 포함 |
표 5. 운영 e2e 에서 로컬과 다르게 나온 것
첫 번째는 앱이 아니라 플랫폼이 먼저 거부한 것이다. 거부된 것은 같으니 테스트가 플랫폼의 403 도 거부로 인정하도록 고쳤다.
두 번째는 테스트 설계가 운영과 맞지 않았다. 테스트 클라이언트는 요청마다 다른 X-Forwarded-For 값을 넣어 IP 를 나누려 했지만 Vercel 은 이 헤더를 실제 IP 로 덮어쓴다. DB 의 refresh_tokens.ip 를 조회하니 실제 IP 하나만 남아 있었다. 그래서 S4·S19 의 실패 로그인이 같은 IP 의 1분 5회 제한을 채웠고 뒤이어 돈 I-1 의 로그인이 429 가 됐다. 성공한 로그인은 횟수에 들어가지 않는 것도 확인했다. I-1 은 1분 쉬고 따로 돌려 통과했다. docs/verify/auth.md 는 이것을 "trust proxy 를 켜도 로그인 제한 키를 위조할 수 없다" 로 적었다.
따로 돌리려다 하나 더 알게 됐다. --test-name-pattern 은 파일 인자 앞에 둬야 한다. 뒤에 두면 필터가 적용되지 않는다.
# ✅ 플래그를 파일 앞에
pnpm exec tsx --test --test-name-pattern="I-1" test/e2e.test.ts
# ❌ 파일 뒤에 두면 필터되지 않는다
pnpm exec tsx --test test/e2e.test.ts --test-name-pattern="I-1"
3-1. 운영 테스트 데이터 정리
운영 e2e 는 운영 DB 에 계정과 매장을 만든다. 돌린 뒤에는 @test.dev 사용자와 그 사용자만 속한 행만 지웠다. 테스트 계정이 아닌 사용자와 매장은 건드리지 않았다.
| 언제 | 지운 것 | 남긴 것 |
|---|---|---|
| 첫 운영 e2e | 사용자 24, 매장 2 | (운영 사용자 0 이 됨) |
| 쿠키 세션 전환 뒤 | 사용자 16, 매장 6 | 테스트 계정이 아닌 사용자 2·매장 1 |
| 유급 선택 제거 뒤 | 사용자 4, 매장 2 | 실제 사용자 2·매장 1 |
| 가이드·로고 배포 뒤 | 사용자 2, 매장 1 | 실제 사용자 2·매장 1 |
| 근무 시간표 배포 뒤 | 사용자 3, 그들만의 매장 2 | 사용자 3·매장 1 |
표 6. 운영 e2e 뒤 지운 테스트 데이터 (히스토리 기록)
4. 로그인 화면을 못 보는 대신 — 가이드 예시를 iframe 으로 잰다
화면 검증에는 두 가지 제약이 있었다.
첫째, 브라우저 자동화로는 계정을 만들거나 비밀번호를 넣지 않는다. 그래서 로그인해야 보이는 멤버·사장 화면을 자동화로 열 수 없다.
둘째, 자동화 환경에서 브라우저 창 크기 조절이 적용되지 않았다. 모바일 폭(390px)을 보려고 창을 줄여도 그대로였다.
두 가지를 이렇게 돌아갔다. 로그인 없이 열리는 운영 컴포넌트 가이드(/guide, 06편)에는 앱과 같은 컴포넌트·같은 CSS 로 그린 예시가 있다. 운영 페이지 안에 width: 390px(또는 320px) iframe 을 만들고 그 안에 가이드 페이지를 띄워서 요소의 크기를 쟀다. iframe 안의 페이지는 iframe 폭을 화면 폭으로 알고 그린다.
| 단계 | 무엇 |
|---|---|
| 1 | 운영 페이지를 연다 |
| 2 | 그 안에 390px·320px iframe 을 만든다 |
| 3 | iframe 안에 가이드 페이지(재현할 컴포넌트 예시)를 띄운다 |
| 4 | getBoundingClientRect()·scrollWidth 로 높이·넘침을 잰다 |
표 7. iframe 으로 모바일 폭을 재는 순서
이 방법으로 잰 것들이다.
| 무엇 | 잰 값 |
|---|---|
| 가이드 17쪽 문서 넘침 | 처음 /guide/foundations 437px(390px 틀) → 수정 뒤 넘치는 쪽 0 |
입력칸 .field (요청 화면 대신 Field 예시) | 수정 전 select 43 / input 46 → 수정 뒤 전부 44, 두 칸 겹침 0 |
| 멤버 시계 | 옛 표기 2줄 → 23:59:59·00:00:00 1줄 |
| 오늘 근무 가이드 3쪽 | 가로 넘침 0(scrollWidth 375/305·309), 시계 10개 높이 48, 금액 6개 한 줄 |
| 스켈레톤 | clock-card 232/232, today-dashboard 426/426, 320px 에서 522/466 |
| 조건 수정 모달 | 여는 버튼 32px, 모달 폭 326, 취소·저장 34px, 가로 넘침 0 |
표 8. 390·320px iframe 으로 잰 값 (히스토리 [검증] 줄)
멤버 시계는 가이드 예시가 아니라, 운영 CSS·글꼴을 쓰는 iframe 안에 멤버 화면 구조(px-5 + 카드 p-5)를 만들어 줄 수를 쟀다.
이 방법에는 한계가 있다. 측정한 것은 가이드 예시이지 로그인한 실제 화면이 아니다. 데스크톱 Chrome 의 iframe 이라 iOS Safari 의 날짜 칸 동작은 재현되지 않았다. 사용자가 휴대폰에서 본 날짜 칸 겹침은 이 방법으로 재현되지 않았고 재현되지 않았다는 사실을 그대로 적었다. 측정에 쓴 스크립트는 저장소에 남아 있지 않다.
5. 성능 측정 — 스크립트와 Resource Timing
"데이터 로드부터 ui 표시까지" 를 재 달라는 요청에 두 층으로 쟀다(docs/verify/performance.md).
5-1. API — perf-boot.mts
server/scripts/perf-boot.mts 는 멤버 앱이 켜질 때 부르는 순서를 그대로 흉내 낸다. /users/me 로 로그인을 확인하고 이어서 기록·휴가·요청·동료 4개를 병렬로 부른다. 웹 주소 경유와 API 직접 호출을 각 10회 돌린다.
async function boot(c: C) {
const t0 = performance.now();
const a = await c.req("GET", paths.me);
const [s, ab, r, co] = await Promise.all([c.req("GET", paths.shifts), c.req("GET", paths.absences), c.req("GET", paths.requests), c.req("GET", paths.colleagues)]);
return { total: performance.now() - t0, me: a.ms, shifts: s.ms, absences: ab.ms, requests: r.ms, colleagues: co.ms, bytes: s.bytes, vid: a.vid, status: [a, s, ab, r, co].map((x) => x.status).join(",") };
}
측정 전에 운영 DB 에 @test.dev 사장·멤버를 만들고 멤버에게 두 달치 평일 근무 기록 40건을 넣는다. 응답 헤더 x-vercel-id 도 같이 기록해서 요청이 어느 리전에서 처리됐는지 본다(07편의 icn1::iad1). 스크립트 주석에는 끝나면 계정·매장을 지운다고 적혀 있지만 스크립트 자체에 지우는 코드는 없다. 히스토리에는 측정이 끝난 뒤 지웠다고 적혀 있다.
cd server && DATABASE_URL=<운영> N=10 pnpm exec tsx scripts/perf-boot.mts
결과는 멤버 부팅 합계 p50 563ms(웹 경유)·p90 1088ms, DB 를 거치지 않는 /health p50 214ms 였다.
5-2. 화면 — Resource Timing 과 layout-shift
화면 쪽은 브라우저에 이미 로그인돼 있던 사장 세션으로 쟀다. 비밀번호를 넣지 않았고 읽기만 했다. 사장 화면 5개를 iframe 으로 각 5회 열고 브라우저가 남기는 기록을 읽었다.
performance.getEntriesByType("navigation")— HTML 문서가 언제 도착했나(TTFB, DOMContentLoaded)performance.getEntriesByType("resource")— 화면이 부른 API 가 각각 언제 시작해 언제 끝났나(Resource Timing)layout-shift기록 — 화면이 밀린 양(CLS)
| 화면 | HTML TTFB | /users/me 끝 | 마지막 API (p50) | CLS |
|---|---|---|---|---|
/admin 대시보드 | 14 | 334 | 832 | 0 |
/admin/calendar 달력 | 14 | 320 | 914 | 0 |
/admin/requests 요청 | 14 | 312 | 739 | 0 |
/admin/members 멤버 | 13 | 325 | 827 | 0 |
/admin/store 매장 | 14 | 339 | 642 | 0 |
표 9. 사장 화면별 데이터 도착 시점 (ms, 문서 요청 시작 기준, 5회 중앙값)
화면이 데이터를 그리는 순간을 직접 재지는 못했다. 대신 "마지막 API 응답이 끝난 시점" 을 썼다. React 는 그 직후 데이터를 그린다.
측정 도중 두 번 막혔다.
- 도구 제한 45초. 9회 측정을 스크립트 호출 한 번으로 기다리다가 브라우저 자동화 도구의 45초 제한을 넘었다. 측정은 페이지 안에서 돌려 두고 결과만 나중에 여러 번 읽어 오는 방식으로 바꿨다.
- 보이지 않는 탭. 처음에는
requestAnimationFrame으로 스켈레톤이 사라지는 순간을 재려 했다. 그런데 측정 탭이 화면 뒤에 가려진(hidden) 상태라 브라우저가requestAnimationFrame을 돌리지 않았다. 같은 이유로 FCP·LCP(첫 글자·가장 큰 요소가 그려진 시점)도 기록이 남지 않았다. 그래서 Resource Timing 의 마지막 API 응답 시점으로 대신했다.
6. 검증하지 못한 것
| 항목 | 상태 | 이유 |
|---|---|---|
| 로그인한 멤버·사장 실화면 | 미검증 | 자동화로 계정 생성·비밀번호 입력을 하지 않는다. 가이드 예시와 iframe 으로 대신 |
| 멤버 화면 성능 실측 | 미검증 | 브라우저에 멤버 세션이 없어 API 부팅 순서(perf-boot.mts)로 대신 |
| 브라우저 시나리오 S20~S22 | 미검증 | 새로고침 뒤 로그인 유지, 탭 두 개 동시 만료, 로그아웃 동기화. 사람 확인 필요 |
| iOS Safari 날짜 칸 | 미검증 | 실기기가 없다. 데스크톱 Chrome iframe 에서는 재현 안 됨 |
| 실기기 전반 | 미검증 | 모든 모바일 측정이 데스크톱 브라우저 iframe 이다 |
| FCP·LCP | 측정 불가 | 측정 탭이 hidden 이라 paint 기록이 없다 |
| 느린 모바일 회선 | 미측정 | 개발 PC 유선 회선에서만 쟀다 |
| 라이트 모드 화면 | 미검증 | 버튼 색 등은 다크 모드에서 쟀다 |
| 독립 QA | 미실시 | QA 역할 에이전트를 띄우지 않았다. 다시 돌린 검사는 자기 검토라 QA 로 치지 않는다 |
| 세션 상한 S17 운영 | 로컬만 | 운영 설정을 바꿔야 해서 |
표 10. 검증 한계
단위 테스트와 e2e 는 숫자로 통과를 확인할 수 있었다. 화면과 기기 쪽은 운영 가이드와 iframe 으로 좁혀 갈 수 있는 데까지만 쟀고 나머지는 위 표대로 사람이 휴대폰과 로그인한 화면으로 확인해야 하는 항목으로 남아 있다.
이전 편: [타임시트] Vercel 배포 — 웹과 API 를 따로 올리고 리전을 재다 (07)
처음으로: [타임시트] 스택과 구조 안내 (00)