2026.08.14#quality6분

로그에 안 남는 버그, 그림을 좌표로 비교해서 잡기

선생님이 화면에서 본 카드와 학생이 받는 PNG는 서로 다른 코드가 그립니다. 한쪽에만 수정이 적용되면 에러 없이 조용히 다른 그림이 전달됩니다. 가짜 캔버스로 두 경로의 좌표를 기록해 비교하는 테스트를 만들었습니다.

같은 그림을 그리는 코드가 두 벌

발음 교정 서비스의 피드백 카드는 두 번 그려집니다. 선생님이 화면에서 첨삭할 때는 라이브 렌더 코드가 캔버스에 그리고, 학생에게 전달할 때는 별도의 export 코드가 PNG로 굽습니다. 두 코드는 같은 717×423 좌표계를 쓰므로, 산점도의 점과 곡선의 좌표는 완전히 일치해야 합니다.

선생님은 자기 화면의 곡선 위에 그림과 영상으로 첨삭합니다. 학생이 받는 PNG의 곡선이 조금 다른 위치에 있다면, 첨삭이 가리키는 지점과 실제 곡선이 어긋난 카드가 전달됩니다.

예외도 로그도 없는 실패

실제로 한 번 일어났습니다. 피치 보정 로직을 수정하면서 한쪽 렌더 경로에만 반영됐고, 곡선이 점 무리를 벗어난 카드가 나갔습니다. 별도의 사건도 있었습니다. 음절 글자를 라이브 화면은 HTML로, export는 캔버스로 그리는데, export 쪽 글자 그리기가 빠지면서 글자 없는 카드가 저장된 적도 있습니다.

두 사건의 공통점은 어떤 로그에도 남지 않았다는 것입니다. 예외가 발생하지 않았고, 응답 코드도 정상이었고, 모니터링 지표도 움직이지 않았습니다. 앱 입장에서는 그림이 조금 다를 뿐 전부 성공이었습니다. 이런 부류의 버그는 검수하는 사람의 눈에 걸리기 전까지 존재 자체를 알 수 없습니다.

가짜 캔버스로 두 경로의 좌표를 기록하기

그래서 두 렌더 경로가 같은 좌표를 그리는지 자동으로 대조하는 테스트를 만들었습니다. 방법은 그리기 명령을 가로채는 가짜 캔버스 컨텍스트입니다.

const createRecorder = (): Recorder => {
  const arcs: Arc[] = []
  const beziers: Bezier[] = []
  const moveTos: Array<{ x: number; y: number }> = []
  const texts: Text[] = []

  const ctx = {
    // 좌표를 남기는 것들
    arc: (x: number, y: number, r: number) => arcs.push({ x, y, r }),
    bezierCurveTo: (...args: number[]) => beziers.push(args),
    moveTo: (x: number, y: number) => moveTos.push({ x, y }),
    // ...
  }
}

진짜 캔버스 대신 이 recorder를 라이브 렌더 함수와 export 렌더 함수에 각각 넘기면, 두 함수가 어떤 점(arc)과 곡선(bezier)을 어디에 그리려 했는지가 배열로 남습니다. 테스트는 그 배열이 서로 완전히 같은지 단언합니다. 픽셀을 비교하는 스냅샷 테스트와 달리 렌더링 환경이 필요 없고, 어긋나면 어느 점이 몇 px 다른지 좌표로 바로 보입니다.

글자도 기록합니다. 음절 글자는 export 경로에만 캔버스로 그려지는데, 이걸 기록하지 않으면 앞서 말한 "글자가 빠진 카드"를 못 잡기 때문입니다.

보정 로직을 한곳으로 모으기

테스트는 어긋남을 잡아주지만, 애초에 어긋날 수 없는 구조가 더 낫습니다. 그래서 피치 보정 같은 데이터 가공은 두 렌더 경로가 갈라지기 전에 한 번만 합니다. 옥타브 에러 접기도 이 위치에 있어서 라이브 렌더와 export 렌더가 같은 교정 결과를 받습니다.

그래도 한쪽 렌더만 고치는 실수는 나올 수 있습니다. 그런 어긋남은 좌표 대조 테스트에서 걸립니다.

같은 결과를 내야 하는 코드가 두 벌인 곳이면 이 방식이 그대로 통합니다. 화면과 PDF, 서버 계산과 클라이언트 계산도 사정이 같습니다. 산출물 비교가 어려우면 산출물을 만드는 명령이나 중간값을 기록해 대조하면 되고, 두 경로가 서로 같은지만 보므로 기대값을 따로 만들 필요도 없습니다.

©2026 Imjurney · All Rights Reserved8 Posts | 4 Projects