Devin.KR

대기·시간·공유 상태 수정

110분 안팎

학습 목표

실패 조건을 고정해 재현하고 원인별로 수정합니다.

개념

실패 조건을 먼저 고정합니다

프로필 저장을 누른 뒤 잠깐 기다리는 검사가 빠른 환경에서는 통과하고 느린 환경에서는 실패합니다. 기다리는 시간을 더하면 당장은 통과할 수 있지만 언제 저장이 끝났는지는 여전히 관찰하지 않습니다. 이번 레슨은 지연을 가상 시계로 고정해 실패를 재현한 다음, 완료 상태를 기다리는 방식으로 검사 코드를 수정합니다. 제품의 저장 계약은 그대로 유지합니다.

qa-flaky-repair에는 engine.cjs, repair.cjs, test.cjs, replay.cjs가 있습니다. engine은 통제할 입력과 관찰 가능한 상태를 제공하고 repair는 학습자가 고치는 검사 전략입니다. test는 기대 결과와 실패 주입을 확인합니다. replay는 seed 한 개의 관찰을 JSON으로 출력합니다. 테스트 기대를 바꾸면 불안정성을 해결한 것이 아니라 확인 기준을 바꾼 것이 됩니다.

scenario의 seed는 0~999 정수입니다. 지연은 seed를 4로 나눈 나머지이고 짝수는 write→read, 홀수는 read→write 순서입니다. seed 2는 지연 2에 정방향이고 seed 0은 지연 0에 정방향입니다. 난수를 사용하지 않아 수정 전후가 같은 조건으로 재현됩니다. 이 seed 계약은 모형에만 적용되며 실제 UI 실행 순서를 대체하지 않습니다.

가상 시계로 기다린 상태를 드러냅니다

page는 tick 0에서 시작합니다. advance를 한 번 호출하면 tick이 1 증가합니다. ready는 현재 tick이 delay 이상인지를 알려 주고 value는 준비 전에는 이전이름, 준비 후에는 새이름을 반환합니다. 한 tick을 1밀리초라고 해석하지 않습니다. 실제 렌더링이나 OS 스케줄링이 섞이지 않도록 만든 결정적인 모형입니다. 시간이 관찰값에 미치는 영향만 분리해 확인합니다.

starter의 wait는 advance를 한 번 호출하고 종료합니다. 지연이 0이나 1이면 새이름을 관찰할 수 있고 지연이 2나 3이면 SAVE_NOT_READY가 남습니다. 오류가 난 seed를 유지한 채 wait 구현만 수정하여 두 시도를 비교합니다. 실행이 느렸다는 설명을 넘어서 준비되지 않은 값을 읽은 시점과 필요한 상태를 코드로 설명할 수 있어야 합니다.

waitFor는 현재 상태를 먼저 확인하고 조건이 충족되지 않으면 가상 시계를 한 tick씩 진행합니다. budget이 3이면 tick 0, 1, 2, 3에서 관찰할 수 있습니다. 마지막에도 조건이 거짓이면 WAIT_TIMEOUT을 발생시킵니다. 지연 0은 불필요한 진행 없이 완료하고 지연 3은 마지막 경계에서 성공합니다. 지연 4는 범위 밖이므로 오류를 유지합니다.

상태와 저장값을 함께 기다립니다

완료 조건은 ready가 참이고 value가 새이름인 조합입니다. 준비 표시만 기다리면 잘못된 닉네임이 나타나도 통과할 수 있습니다. test는 준비 표시가 참이지만 값이 잘못된 모형을 넣어 WAIT_TIMEOUT을 확인합니다. ready와 저장값은 같은 요구를 다른 측면에서 관찰합니다. 실제 UI에서도 로딩 표시가 사라진 것만으로 저장 성공이라고 결론 내리지 않습니다.

실제 브라우저의 대기는 앞 모듈 actions.cjs에 있습니다. 저장 완료 상태와 닉네임을 각각 단언하도록 구성되어 있습니다. 이번 repair는 그 흐름의 원리를 작은 모형으로 분리한 연습입니다. 실제 API 저장이 끝났는지와 화면에 반영되었는지는 추가 관찰이 필요합니다. 가상 시계의 열 seed가 통과했다고 실제 DOM 지연 조건을 실행했다는 표현을 쓰지 않습니다.

최대 대기를 없애면 영구 로딩이나 잘못된 값에서도 러너가 끝나지 않습니다. 제한 시간을 늘리는 조치는 예상 소요와 측정 근거가 있을 때 검토할 수 있지만 잘못된 조건식을 고치는 대신 사용하지 않습니다. WAIT_TIMEOUT이 나면 seed, 마지막 관찰값, 기다린 조건과 한도를 보고합니다. 부족한 정보는 새 관찰 필드로 보완하며 오류를 성공으로 바꾸지 않습니다.

실행 순서 의존을 제거합니다

상태 모형에는 기존 데이터 keep과 검사 계정이 있습니다. write 검사는 닉네임을 새이름으로 바꾸고 read 검사는 자신의 초기 닉네임이 이전이름인지를 확인합니다. shared라는 같은 키를 쓰면 write 다음 read에서 STATE_LEAK가 발생합니다. 역방향은 우연히 통과하여 순서 의존을 드러냅니다. 기대값이 다른 두 검사가 같은 상태를 공유한 것이 원인입니다.

key 함수는 실행 seed와 검사 ID를 함께 사용합니다. r2.write와 r2.read는 같은 실행의 다른 검사이며 r3.read는 다른 실행의 검사입니다. 검사마다 생성된 키에 이전이름을 준비하고 자신이 소유한 키만 정리합니다. 실행 ID의 예약 정책은 실제 시스템의 fixture가 맡습니다. 여기서는 서로 다른 seed와 검사 ID가 서로 다른 key라는 제한된 계약을 확인합니다.

cleanup을 true로 바꾸면 engine의 finally에서 검사 소유 key를 삭제합니다. 준비와 검증을 같은 블록으로 묶어 실패해도 정리가 호출되도록 합니다. keep은 다른 실행의 기존 데이터이므로 건드리지 않습니다. 공유 계정을 새로 만들기만 하고 정리를 생략하면 장시간 반복에서 누적 상태가 남을 수 있습니다. 격리와 소유 범위 정리는 함께 검토합니다.

수정의 증거를 단계별로 쌓습니다

수정 전 replay 2 baseline은 SAVE_NOT_READY이고 replay 0 baseline은 STATE_LEAK입니다. 대기만 고치면 지연 실패는 없어져도 정방향 데이터 실패가 남습니다. 이때 두 원인을 한 번에 같은 이름으로 처리하지 않습니다. 대기 수정과 key·cleanup 수정을 각각 설명하고 같은 대표 seed로 확인합니다. seed 1의 통과가 seed 0의 실패를 지우지 않습니다.

bash check.sh는 0~9 seed의 baseline 오류가 약속된 패턴인지 먼저 비교하고 repair의 같은 seed가 최초 시도에서 통과하는지 확인합니다. 실행별 key와 검사별 key 차이, cleanup 설정, 잘못된 seed, 준비 경계와 최대 대기 초과도 확인합니다. 반복 재시도 루프는 없습니다. 오류를 실제로 발생시키면서 예상 오류와 비교하는 테스트는 검사 러너 안에서 통과할 수 있습니다.

AssertionError에 seed=0 STATE_LEAK가 있으면 key 공유 또는 cleanup을 확인합니다. seed=2 SAVE_NOT_READY면 wait 구현을 읽습니다. WAIT_TIMEOUT이 정상 지연 3에서 나오면 마지막 tick의 관찰을 누락했는지 봅니다. 지연 4가 통과했다고 나오면 한도를 없앴거나 오류를 삼켰는지 확인합니다. 메시지의 seed와 오류 코드는 코드 위치를 좁히는 도구입니다.

수정 범위와 남은 관찰을 구분합니다

이전 UI fixture는 실패 뒤에도 소유 데이터를 제거하는 withFixture를 사용합니다. 실제 앱에서 공유 계정 결함이 발생하면 fixture 생성·정리와 병렬 실행의 ID 예약부터 조사합니다. 이번 모형의 Map 정리는 DB 삭제를 확인한 것이 아닙니다. 실제 데이터 정리의 안전성은 앞 모듈의 API·저장 검증과 연결하여 설명합니다. 작은 단위 검사가 확인한 범위를 정확히 말합니다.

시간에 의존하는 세션 검사에는 현재 시각을 주입하는 방법이 유용합니다. 만료 직전과 정확한 만료 시각, 직후를 입력으로 고정하면 기다리다가 우연히 경계를 넘는 검사를 피할 수 있습니다. 이번 레슨은 저장 대기 모형을 직접 고치고 세션 시계의 입력 설계는 다음 레슨의 회귀 사례로 이어 갑니다. 실제 인증 시간을 모형의 tick과 같은 단위로 섞지 않습니다.

완료 후 repair.cjs의 변경 이유를 세 문장으로 적습니다. 어떤 상태를 기다렸는지, key가 왜 겹치지 않는지, 실패해도 어느 데이터를 정리하는지가 포함되어야 합니다. 같은 seed 전후 결과와 최대 대기 실패 증거를 연결합니다. 이벤트 루프 일반 원리는 더 읽기에서 보완하고 여기서는 기다림의 대상과 격리 범위를 직접 설명하는 능력을 확인합니다.

따라하기

seed의 재현 조건을 읽습니다

지연과 순서를 계산합니다. 결과는 실제 브라우저 응답 시간이 아니라 가상 모형 입력입니다.

for(const seed of [0,1,2,3])console.log(seed,'delay='+seed%4,seed%2===0?'write,read':'read,write');

실행 결과

0 delay=0 write,read
1 delay=1 read,write
2 delay=2 write,read
3 delay=3 read,write

준비 경계를 비교합니다

한 tick 대기로 지연 2의 준비 상태에 도달하지 못하는 이유를 확인합니다.

for(const delay of [0,1,2,3])console.log('delay='+delay,'readyAfterOne='+(1>=delay));

실행 결과

delay=0 readyAfterOne=true
delay=1 readyAfterOne=true
delay=2 readyAfterOne=false
delay=3 readyAfterOne=false

소유 키를 나눕니다

검사 ID와 실행 ID를 둘 다 키에 넣습니다. 기존 keep 계정은 생성 목록에 넣지 않습니다.

const key=(seed,id)=>`r${seed}.${id}`; console.log(key(2,'write'));console.log(key(2,'read'));console.log(key(3,'read'));

실행 결과

r2.write
r2.read
r3.read

수정 전후를 로컬에서 실행합니다

압축을 푼 루트에서 node replay.cjs 2 baseline, node replay.cjs 0 baseline을 각각 실행합니다. 둘은 의도적으로 종료 코드 1입니다. repair.cjs 세 TODO를 완성하고 node replay.cjs 2 fixed와 bash check.sh를 실행합니다. 성공 기준은 10 seeds passed 문구와 종료 코드 0입니다. 최대 대기 경계 테스트도 유지합니다.

확인 문제

실습

repair.cjs의 wait를 waitFor로 바꾸어 ready와 새이름을 budget 3까지 기다립니다. key는 seed·검사 ID를 구분하고 cleanup은 true로 둡니다. engine과 테스트 기대는 유지합니다. starter는 지연·순서 단언 일부가 실패하고 solution은 0~9 seed와 경계 검사를 통과합니다. 이 실습은 실제 UI 대신 가상 시계·Map을 사용합니다.

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

실행 명령

bash check.sh

기대 결과

종료 코드 0; 10 seeds passed; isolation, cleanup, timeout boundaries passed

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

더 읽기

면접 질문

  • UI 자동화에서 고정 시간 대기를 줄이는 방법을 설명해 주시면 됩니다.
  • 실행 순서에 따라 결과가 달라지는 테스트를 확인하는 방법을 설명해 주시면 됩니다.