- Published on
기능목록 엑셀을 테스트로 돌렸다 — 메뉴 136종 QA 스윕
- Authors

- Name
- Hyo814
기능목록 엑셀을 테스트로 돌렸다 — 메뉴 136종 QA 스윕
이 프로젝트에는 기능목록이라는 납품 문서가 있습니다. 메뉴 이름과 URL이 줄줄이 적힌 엑셀이에요. 시험절차서를 쓸 때 이 목록을 보고 화면을 하나씩 열어봅니다.
136개입니다. 사용자 화면 50개, 관리자 화면 86개.
손으로 다 열어보고, 캡처 뜨고, 문서에 붙입니다. 화면을 하나 고칠 때마다요.
1. 이걸 테스트로 볼 수 있나
처음엔 이게 테스트인지 확신이 없었습니다. 단정할 수 있는 게 "200이 뜬다" 정도니까요.
그런데 실제로 이 프로젝트에서 나온 사고를 되짚어보니 대부분이 "그 화면을 안 열어봐서" 였습니다. 라우팅을 고치고 옆 화면이 죽거나, 템플릿 공통 매크로를 고치고 그걸 쓰는 화면 셋 중 하나만 확인하거나요. 카탈로그 다운로드 4종이 죽어 있던 것도 아무도 그 버튼을 안 눌러봐서 몇 달을 갔습니다.
"전 화면이 열리긴 하는가"는 낮은 기준이지만, 우리가 실제로 깨뜨리던 건 그 기준이었습니다.
그래서 목표를 낮게 잡았습니다. 흐름 검증은 따로 두고, 스윕은 열리는지 + 캡처만.
| 층 | 대상 | 확인하는 것 |
|---|---|---|
| QA 스윕 | 메뉴 136종 | 상태코드 < 400, 404/500 화면 아님, 전체 캡처 |
| 시나리오 | 주요 흐름 | 클릭·입력·저장 후 결과 |
| flows | CRUD 6종 | 공지·태그·분류체계·기관구성원·OID 신청 |
2. 엑셀을 JSON으로 한 번 내린다
기능목록은 엑셀입니다. 테스트가 매번 엑셀을 읽게 할 수도 있었는데 안 그랬어요.
| 방법 | 판단 |
|---|---|
| 스펙에서 xlsx 직접 읽기 | JS에서 xlsx 파서 의존성이 붙음. 테스트가 문서 포맷에 묶임 |
| DB나 URLconf에서 메뉴 자동 수집 | 문서와 대조하는 게 목적인데, 코드에서 뽑으면 코드끼리 대조가 됨 |
파이썬 스크립트로 xlsx → menu-list.json 추출 | 채택 |
세 번째를 고른 결정적 이유는 두 번째의 반례입니다. 이 스윕의 가치는 "문서에 적힌 URL"과 "실제 라우팅"을 부딪히게 하는 데 있어요. 라우팅에서 메뉴를 뽑으면 항상 통과하는 테스트가 됩니다.
// 목록 갱신: python scripts/extract_qa_menu.py
const MENU = JSON.parse(
fs.readFileSync(new URL("./menu-list.json", import.meta.url), "utf8"),
);
menu-list.json은 커밋합니다. 문서가 개정되면 스크립트를 다시 돌리고, 그 diff가 곧 "문서가 이만큼 바뀌었다" 는 기록이 돼요.
3. 부딪혀보니 문서가 15군데 틀렸다
돌리자마자 문서와 라우팅이 어긋난 항목이 쏟아졌습니다. 이걸 어떻게 다룰지가 이 스윕에서 제일 오래 고민한 부분이에요.
// 기능목록 v1.6 의 URL 이 현재 라우팅과 다른 항목 — 문서 갱신 대상 목록이기도 하다
const URL_FIXES = {
"/workflow/request": "/workflow/wf-dataset/request/",
"/workflow/qualify": "/workflow/wf-dataset/qualify/",
"/web-admin/system/survey/": "/web-admin/survey/responses/",
"/web-admin/workflow/post-qualify/": "/web-admin/workflow/wf-dataset/post-qual/",
// 분류체계 다운로드는 admin 라우트가 없어 공개 라우트만 존재
"/web-admin/std-data/scheme/<int:pk>/download_rdf":
"/std-data/scheme/<int:pk>/download_rdf",
...
};
| 방법 | 얻는 것 | 포기하는 것 | 판단 |
|---|---|---|---|
| ① 엑셀 원본을 고쳐서 맞춤 | 목록이 깨끗해짐 | 문서는 납품물이라 개정 절차가 있음. 우리가 못 고침 | 기각 |
| ② 안 맞는 항목을 목록에서 뺌 | 테스트가 전부 초록 | 어긋났다는 사실이 사라짐. 다음 개정 때 또 발견 | 기각 |
| ③ 코드에 보정 맵을 두고 "문서 갱신 대상 목록"이라고 적음 | 테스트도 돌고 어긋난 목록도 남음 | 맵을 사람이 관리해야 함 | 채택 |
②의 유혹이 컸습니다. 목록에서 빼면 깔끔해지니까요. 그런데 "안 맞는 걸 지워서 초록으로 만드는 것"이 정확히 이 스윕이 막으려던 일이었어요.
URL_FIXES는 보정 맵이면서 동시에 다음 문서 개정 때 고칠 항목 15건의 목록입니다. 주석 한 줄이 그 두 번째 역할을 부여해요. 그 주석이 없으면 6개월 뒤엔 "왜 이런 맵이 있지"가 됩니다.
4. <int:pk>를 어떻게 채우나
문서의 URL에는 <int:pk>가 그대로 들어 있습니다. 상세 화면이니까요.
// 문서의 <int:pk> 를 초기 적재 데이터의 실존 pk 로 치환 (긴 prefix 우선 매칭)
const KNOWN_PKS = [
["/web-admin/std-data/meta-class-property", 1],
["/web-admin/std-data/meta-class", 35],
["/web-admin/std-data/concept", 34],
...
];
긴 prefix를 먼저 매칭하는 게 요점입니다. /meta-class와 /meta-class-property가 둘 다 있는데, 짧은 쪽이 먼저 걸리면 meta-class-property 상세가 meta-class pk로 열려요. 배열 순서로 우선순위를 표현했고, 그래서 이건 객체가 아니라 배열입니다.
pk를 1이 아니라 35, 34로 박은 데는 이유가 있습니다. 초기 적재 데이터에 pk 1이 없거나 삭제된 행이라 404가 나는 경우가 있었어요. 실제로 열리는 pk를 확인해서 박았습니다. 이건 시드 데이터에 묶인 값이라, 시드가 바뀌면 같이 깨집니다 — 이 스윕의 가장 약한 고리예요.
5. GET인데 위험한 URL이 있다
전 메뉴를 page.goto()로 여는 스윕이라 GET만으로 부작용이 나는 URL을 걸러야 했습니다.
// GET 만으로도 위험하거나(삭제·심사 액션·변환 처리) 화면이 아닌 것은 스윕에서 제외
const SKIP_PATTERNS = [/\/delete\b/, /\/action\//, /asn1-process/];
기능목록에 /delete URL이 들어 있습니다. 문서상으로는 "삭제 기능"이라 목록에 있는 게 맞아요. 그런데 스윕이 그걸 열면 데이터가 지워집니다.
여기서 알게 된 게 하나 있습니다. GET에 부작용이 있는 URL이 존재한다는 것 자체가 설계 문제예요. 크롤러나 브라우저 프리페치가 한 번 지나가면 같은 일이 납니다. 이번엔 테스트에서 제외하는 것으로 끝냈지만, 이건 테스트가 발견한 진짜 결함을 회피로 덮은 것입니다. 스윕 코드에 SKIP_PATTERNS가 남아 있는 한 이 사실이 잊히진 않을 텐데, 고친 건 아니에요.
6. 스킵을 남겨둔 게 맞나
이 스윕에는 test.skip이 여러 종류로 들어 있습니다.
test(title, async ({ page }) => {
if (DOC_OUTDATED[row.url]) {
test.skip(true, DOC_OUTDATED[row.url]);
}
if (SKIP_PATTERNS.some((p) => p.test(row.url))) {
test.skip(true, "GET 부작용 위험(삭제·액션) — 수동 확인 대상");
}
const url = resolveUrl(row.url);
if (!url) {
test.skip(true, "URL 파라미터 대상 데이터 없음 — 수동 확인 대상");
}
...
// 현행 라우팅에 대응 화면이 없거나 대상 데이터가 없어 자동 확인 불가한 항목
const DOC_OUTDATED = {
"/std-data/semantic-search/": "라우트 없음 — 의미 기반 검색 미배포/제거",
"/workflow/dataset/add": "통합 신청 화면으로 개편 — 구 URL 폐기",
"/web-admin/system/survey/<int:pk>": "설문 응답 데이터 0건 — 수동 확인",
...
};
스킵된 테스트는 통과한 테스트가 아닙니다. 보통은 이걸 부채로 취급해야 해요. 그런데 여기선 일부러 남겼고, 그 판단의 근거는 이렇습니다.
- 스킵마다 사유 문자열이 붙어 있고, 리포터가 그걸 출력합니다. 목록에서 지우면 안 보이지만 스킵은 매번 보여요.
- 사유가 행동으로 이어집니다. "라우트 없음"은 문서 갱신, "데이터 0건"은 수동 확인, "GET 부작용"은 코드 수정 대상이에요.
- 즉 여기서 스킵은 "이 케이스는 없는 셈 치자"가 아니라 "자동으로 못 하니 사람이 해라" 라는 지시입니다.
그래도 이건 타협입니다. 정직하게 말하면 이래요.
- 스킵 사유가 문자열이라 아무도 세지 않습니다. "수동 확인 대상"이 몇 개인지, 지난주보다 늘었는지 줄었는지 아무 데도 안 나와요. 스킵 건수 상한을 걸든지, 사유별 집계를 리포트에 찍든지 해야 진짜 지표가 됩니다.
- "설문 응답 데이터 0건"은 시드 데이터를 만들면 자동화할 수 있는데 안 만들었습니다. 못 하는 게 아니라 안 한 거예요.
/delete계열은 5절대로 회피가 아니라 수정이 맞습니다.
7. 설정을 3개로 나눈 이유
playwright.config.js 하나로 갈 수도 있었는데 셋으로 나눴습니다.
| 설정 | 대상 | 다른 점 |
|---|---|---|
playwright.config.js | 시나리오 | public/admin 프로젝트 분리 |
playwright.qa.config.js | 메뉴 스윕 | 뷰포트 1600×900 고정, timeout 45s, 전부 admin 세션 |
playwright.guide.config.js | 이용안내 캡처 | 문서용 캡처 전용 |
나눈 기준은 "실행 목적이 다르면 설정도 다르다" 입니다. 특히 QA 설정은 뷰포트가 고정이에요.
use: {
baseURL: process.env.BASE_URL || "http://localhost:8443",
// 캡쳐본 해상도 — 시험절차서 첨부 기준
viewport: { width: 1600, height: 900 },
},
캡처가 문서에 붙기 때문에 해상도가 흔들리면 안 됩니다. 시나리오 테스트는 반대로 여러 뷰포트에서 돌아야 하고요. 한 설정에 넣으면 둘 중 하나가 양보해야 합니다.
QA 스윕은 일반 사용자 시트까지 전부 admin 세션으로 돕니다. 사용자 메뉴에도 로그인이 필요한 항목(신청·심사)이 섞여 있어서예요. 이건 "익명 사용자가 이 화면을 볼 수 있나"를 확인 못 한다는 뜻이라, 권한 검증은 이 스윕의 범위 밖입니다. 시나리오 쪽 public 프로젝트가 그 역할을 합니다.
8. SQLite 때문에 워커를 1로 박았다
// dev DB가 SQLite라 병렬 워커의 쓰기 시나리오가 "database is locked" 500을 유발
// (신청 생성 등). 읽기 전용만 있어도 안전하도록 전역 직렬 실행.
workers: 1,
Playwright의 기본은 병렬입니다. 그런데 개발 DB가 SQLite라 쓰기 시나리오가 동시에 돌면 database is locked로 500이 납니다.
읽기 전용 스펙만 골라 병렬로 돌릴 수도 있었는데 전역 직렬로 했어요. "이건 병렬해도 되는 스펙"을 사람이 판정해야 하는 구조를 만들면, 언젠가 누가 틀립니다. 그리고 그 실패는 database is locked라는, 원인과 전혀 안 닮은 메시지로 나타나요.
136개를 직렬로 도는 게 느리긴 합니다. 그래도 간헐적으로 실패하는 테스트보다 느린 테스트가 낫습니다. 개발 DB를 PostgreSQL로 맞추면 풀 수 있는 문제인데, 그건 이 작업 범위 밖이라 주석에 이유만 남겼습니다.
9. networkidle이 안 끝난다
캡처 타이밍에서 한 번 걸렸습니다.
// 트리·차트 등 로드 후 ajax 렌더를 기다렸다가 캡쳐
// (browser-reload 의 SSE 폴링 탓에 networkidle 은 끝나지 않음)
await page.waitForTimeout(700);
정석은 waitUntil: "networkidle"입니다. 그런데 개발 서버에 브라우저 자동 새로고침(SSE) 이 붙어 있어요. 서버가 연결을 계속 열어두니 네트워크가 영원히 idle이 안 됩니다.
waitForTimeout(700)은 좋은 코드가 아닙니다. 느린 화면에서는 렌더 전에 찍히고, 빠른 화면에서는 700ms를 그냥 버려요. 대안이 있었는데도 안 골랐습니다.
| 대안 | 안 고른 이유 |
|---|---|
| 화면마다 렌더 완료 셀렉터를 기다림 | 136개 화면마다 셀렉터를 정해야 함. 스윕의 값어치보다 관리 비용이 큼 |
| SSE 요청을 라우팅으로 차단 | 개발 서버 동작을 바꾸는 셈이라 "실제와 같은 화면"이 아니게 됨 |
| 프로덕션 설정으로 서버 띄우기 | 맞는 방향인데 QA용 서버 기동 절차가 따로 생김 |
세 번째가 옳고, 안 했습니다. 지금은 waitForTimeout(700)이 남아 있고 이건 언젠가 느린 화면에서 빈 캡처를 만들 겁니다.
디버그 툴바를 숨기는 것도 같은 성격이에요.
await page.addInitScript(() => {
document.addEventListener("DOMContentLoaded", () => {
const style = document.createElement("style");
style.textContent = "#djDebug{display:none}";
document.head.appendChild(style);
});
});
캡처가 문서에 붙으니 개발 도구가 찍히면 안 됩니다. 이것도 프로덕션 설정으로 띄우면 없어질 코드고요. 같은 원인에서 나온 우회가 둘이라는 건, 원인을 고칠 때가 됐다는 신호입니다.
10. 인프라를 옮기다 발견한 것들
이 코드는 프런트 브랜치에서 이관한 것이라, 옮기면서 나온 게 몇 개 있습니다.
@playwright/test가 package.json에 없었습니다. node_modules에는 있어서 로컬에서는 잘 돌았어요. 선언이 없으니 npm ci 하면 사라집니다. "내 컴퓨터에서는 되는데"의 교과서적 형태였고, 옮기지 않았으면 안 드러났을 겁니다.
이관 다음 날 package-lock.json도 어긋나 있는 걸 발견했습니다. package.json에는 추가했는데 lock이 안 따라와서 npm ci가 실패했어요. 의존성 하나 추가에 파일 두 개가 맞아야 한다는 걸 두 번에 걸쳐 배웠습니다.
.gitignore에 e2e/.auth가 없었습니다.
test-results/
playwright-report/
qa-results/
e2e/.auth/
auth.setup.js가 관리자 로그인 세션을 e2e/.auth/admin.json에 저장합니다. 이게 무시 목록에 없으면 git add . 한 번에 관리자 세션이 저장소로 들어갑니다. 실제로 커밋되진 않았지만, 커밋될 수 있는 상태였어요.
자격증명을 파일로 떨구는 도구를 도입할 땐, 그 경로를
.gitignore에 넣는 것이 도입 절차의 일부입니다. 도구가 알아서 안 해줍니다.
ESLint도 손봐야 했습니다. 스펙과 설정이 Node에서 도는데 프로젝트 ESLint는 브라우저 전역만 알고 있어서 process가 전부 에러였어요.
{
// Playwright 스펙·설정은 Node 환경에서 실행됨
files: ["e2e/**/*.js", "playwright*.config.js"],
languageOptions: { globals: { process: "readonly" } },
}
readonly로 준 게 의도적입니다. 스펙이 process.env를 읽는 건 맞고 쓰는 건 아니에요.
11. 스윕이 아니라 시나리오
스윕은 "열린다"까지입니다. 흐름은 따로 뒀고, 규칙을 문서로 박았습니다.
- 파일 하나 = 사용자 흐름 하나.
- 테스트 제목은 한국어로 "○○ 하면 △△ 가 보인다" 형태.
- 셀렉터를 추측하지 않는다 — 실제 DOM을 확인하고 쓴다.
data-testid는 셀렉터가 불안정한 곳에만. 템플릿을 건드리는 일이라 사용자에게 알린다.- 데이터를 바꾸는 시나리오는 테스트 안에서 원복한다. 못 하면 남는 데이터를 파일 상단 주석에 적는다.
마지막 규칙이 제일 중요합니다. 개발 DB를 공유하니까요. 원복하지 않는 시나리오는 다음 사람의 테스트를 깨뜨립니다. 원복이 불가능한 경우가 실제로 있는데(승인 상태 전이 같은 것), 그럴 땐 숨기지 말고 주석으로 남기게 했어요.
12. 정리
| 항목 | 수 |
|---|---|
| 스윕 대상 메뉴 | 136 (사용자 50 / 관리자 86) |
| 문서 URL 보정 | 15건 (= 문서 갱신 대상) |
| 자동 확인 불가 | 6건 (라우트 없음·데이터 0건) |
| 시나리오 스펙 | 공개 8 + 관리자 6 |
| CRUD flows | 6종 |
- "전 화면이 열리는가"는 낮은 기준이지만, 실제로 깨지던 건 그 기준이었다.
- 문서와 코드를 부딪히게 하려면 목록을 문서에서 가져와야 한다. 코드에서 뽑으면 코드끼리 대조가 된다.
- 안 맞는 항목을 목록에서 지워 초록으로 만들지 않는다. 보정 맵에 사유를 적으면 그게 다음 개정 목록이 된다.
- 간헐적 실패보다 느린 직렬이 낫다. 다만 이유는 주석에 남긴다.
- 자격증명을 파일로 떨구는 도구는
.gitignore가 도입 절차의 일부다. - 그리고 스킵은 사유가 붙어 있어도 부채다. 세지 않으면 늘어난다.