버그 추적

항상 404였던 fetch — 폴백이 버그를 덮고 있었다

타자속도 페이지가 로드될 때마다 404를 냈다. 그런데 기능은 멀쩡했다. 그래서 아무도 몰랐다.

빌드 · fetch · 타자속도

페이지는 멀쩡한데 404가 찍힌다

전 페이지를 자동으로 훑어 콘솔 오류와 4xx 응답을 세는 검사를 돌리다가 발견했습니다. 타자속도 측정기 페이지가 로드될 때마다 404를 하나씩 내고 있었습니다.

요청 대상은 /src/content/typing-texts.json이었습니다. 타자 연습 지문이 담긴 파일입니다. 라이브 사이트에서도 똑같이 404였습니다.

그런데 기능은 멀쩡했습니다. 지문이 정상적으로 나오고, 난이도 탭도 동작하고, 새 지문 버튼도 작동했습니다. 사용자가 이상을 느낄 만한 부분이 하나도 없었습니다.

왜 404인가

정적 사이트 생성기는 소스 디렉터리를 그대로 배포하지 않습니다.src/ 아래 파일들은 빌드 과정에서 처리되어 결과물 디렉터리로 들어가고,원래 경로는 남지 않습니다. 브라우저가 /src/...를 요청할 곳이 없는 겁니다.

런타임에 파일을 받아야 한다면 public/에 두어야 합니다. 거기 있는 파일만 경로 그대로 게시됩니다.

진짜 문제는 그게 아니었다

고치려고 파일을 열었더니, 같은 파일 맨 위에 이미 이게 있었습니다.

import defaultTexts from '../../content/typing-texts.json';

빌드 시점에 번들로 들어가는 정적 import입니다. 그리고 지문 목록을 담는 변수도 처음부터 이 값으로 초기화되고 있었습니다.

let textsList = (Array.isArray(defaultTexts) && defaultTexts.length > 0)
  ? defaultTexts
  : [...FALLBACK_TEXTS];

지문은 처음부터 번들 안에 있었습니다.초기화 함수의 loadRemoteTexts()는 이미 가진 데이터를 네트워크로 다시 받으려는 중복 요청이었던 겁니다. 성공했더라도 결과가 같습니다.

함수를 통째로 지웠습니다. 초기화 함수의 유일한 await이었으므로async도 뗐습니다. 부수적으로 "콘텐츠를 런타임에 받지 않는다"는 프로젝트 규칙 위반도 함께 해소됐습니다.

폴백이 버그를 덮고 있었다

이 버그가 오래 살아남은 이유는 명확합니다. 원래 코드에 폴백이 있었습니다.

} catch (err) {
  // 오프라인이거나 로컬 파일 프로토콜인 경우 FALLBACK 사용
  console.info('Typing texts fallback dataset activated.');
}

의도는 좋습니다. 네트워크가 없어도 도구가 동작하게 하는 방어입니다. 문제는 이 방어가 정상 동작과 구분되지 않는다는 점이었습니다. 요청이 매번 실패하는데 화면은 완벽하게 정상이니 아무도 모릅니다.

게다가 이 경우엔 폴백조차 발동하지 않았습니다. 404는 fetch 입장에서 예외가 아니라 정상 응답이기 때문에catch로 가지 않고 조용히 넘어갔습니다. 원래 값이 유지됐을 뿐입니다.

배운 것

조용히 성공하는 폴백은 위험합니다. 실패를 흡수하면서 아무 흔적을 남기지 않으면 문제가 있는 상태가 영구히 유지됩니다. 적어도 실패했다는 사실이 어딘가에는 남아야 합니다.

그리고 이건 눈으로 볼 수 있는 버그가 아니었습니다. 화면에는 아무 이상이 없었으니까요.4xx 응답과 콘솔 오류를 세는 자동 검사가 없었다면 지금도 모르고 있었을 겁니다. 지금은 배포 전에 모든 페이지에서 이 둘을 0으로 확인합니다.

개발 노트 목록으로