작년에 만났던 문제들과 삽질들

dogyeong·2022년 1월 2일
0

1. 캔버스 차트 회귀 테스트 적용

캔버스로 차트를 그리는 서버를 만들 때였다. 정상적으로 그린 차트 결과물과 같은지 검사하는 테스트를 하면 효율적으로 테스트를 할 수 있을 것이라 생각했다. (나중에 알고보니 이런 테스트를 회귀 테스트라고 한다)

첫 생각은 차트 결과물이 base64 문자열로 나오기 때문에 문자열을 저장해놓고 비교하면 된다고 생각했다. 그런데 로컬에서 그린 결과물과 gitlab 테스트 환경에서 그린 결과물의 base64 문자열이 서로 달랐다.

찾아보니 OS, 브라우저마다 폰트, 이미지를 그리는 방식이 달라서 정확히 똑같은 이미지가 나온다는 보장이 없었다

이 때 삽질하면서 git diff로 확인해본 이미지는 아래와 같다. 다른 부분이 색깔로 표시된다.

그리고 이럴 때 시각적 회귀 테스트를 한다는 것도 알게 되었다. 그래서 jest-image-snapshot 패키지를 활용해서 스냅샷을 저장하고, 테스트하는 환경을 구축했다. 추가로 폰트 파일을 추가해서 동일한 폰트로 그려봤지만 로컬 환경과 CI 환경에 약간의 차이가 있었다.

아래는 jest diff 결과로, 양 쪽은 로컬과 CI환경에서 그린 결과물이고, 가운데는 양 쪽 결과물의 픽셀단위로 차이점을 시각적으로 나타낸 것이다. 눈으로 봤을 때 거의 똑같지만 약간 흐릿하거나 픽셀의 rgb값이 약간 다른 것을 알 수 있었다.

마지막으로, failureThreshold 옵션값을 설정하여 약간의 오차를 허용하도록 해서 테스트를 통과할 수 있게 만들어주었다.

2. 서로 다른 Promise 비동기 로직을 동기적으로 실행하기

일정 시간마다 API를 호출해서 결과값을 캐싱하는 로직의 테스트 코드를 작성할 때였다.

타이머를 mocking해서 시간을 변경해도 캐싱 로직이 수행되지 않아 테스트가 실패하는 문제가 있었다.

해당 코드를 간략하게 표현하면 아래와 같다.

// API 반환값을 설정
cronService.setFetchResult('hello');

// cron job을 수행하도록 타이머 시간을 변경
jest.advanceTimersByTime(Time.ONE_MINUTE);

// 정상적으로 캐싱되었는지 테스트 -> 실패
expect(await cronService.fetchEveryMinute()).toEqual('hello');

모든 코드가 평가된 후에 캐시가 변경된 것을 확인했는데, 이것은 jest.advanceTimersByTime()으로 타이머의 시간을 바꾸면 타이머의 콜백함수가 async 비동기 함수이기 때문에 비동기 함수의 내부 로직을 mocking해서 동기적으로 실행되어도 콜 스택이 빌 때까지 기다렸다가 수행되기 때문인 것으로 파악했다.

결국, 타이머를 변경하고 테스트를 하기 전에 이 비동기 로직을 동기적으로 수행할 필요가 있었다. 구글링을 해 보니 해결법은 의외로 간단했다.

// API 반환값을 설정
cronService.setFetchResult('hello');

// cron job을 수행하도록 타이머 시간을 변경
jest.advanceTimersByTime(Time.ONE_MINUTE);

// 마이크로 태스크 큐를 비워준다
await Promise.resolve().then();

// 정상적으로 캐싱되었는지 테스트 -> 실패
expect(await cronService.fetchEveryMinute()).toEqual('hello');

빈 프로미스와 await를 이용해서 마이크로 태스크 큐를 다 처리할 때까지 동기적으로 실행시켜주는 방법이었다.

재미있는 점은 타이머 콜백함수 내의 await 문 갯수만큼 Promise.resolve() 뒤에 then()을 체이닝해줘야 하는 점이었다.

처음에는 왜 그래야 하는지 이해하지 못했지만, 나중에 다시 생각해보니 간단한 원리였다.

다른 프로미스의 체이닝을 계속 마이크로 태스크 큐에서 번갈아가면서 수행하도록 하는 것인데, 이 링크에서 시각적으로 확인해보자.

3. CSP

아이프레임으로 사용되는 페이지를 개발하는 일을 했었는데, 모든 http 요청이 https로 자동으로 바뀌는 이슈가 있었다.

알고보니 CSP(Content Security Policy)를 설정하기 위해 helmet 패키지를 썼는데, helmet 설정의 기본값 중에 하나가 upgrade-insecure-requests 디렉티브를 사용하는 것이었다.

기본 옵션을 비활성화 함으로써 이슈를 해결했고, 덕분에 CSP 공부하면서 SOP, CORS와의 관계나 배경에 대해 이해할 수 있었던 계기가 되었던 것 같다.

profile
Engineer

0개의 댓글