saewoo
githubemail
2024-7-13

이펙티브 디버깅 적용기

이펙티브 디버깅 템플릿으로 오픈소스 버그 해결하기

문제를 정상화하는 과정인 디버깅은 경험상 대부분의 시간이 코드를 이해하는데 소요 되는것 같습니다.

심지어 90%의 시간을 코드를 이해하는데 소요되고 코드 작성은 10% 정도만 되는 경우도 꽤나 있습니다.

저는 '디버깅 시간을 단축하기 위해선 어떻게 하는 것이 효율적일까?'에 대한 호기심으로 지난 7월 8일 넥스트스텝에서 진행하는 이펙티브 디버깅 2기 워크숍에 참석 했습니다.

강연은 현재 시점의 나의 디버깅 과정을 작성해 보고, 전문가의 디버깅 과정과 템플릿을 습득하여 나만의 디버깅 템플릿을 만드는 과정의 연속이었습니다.

이펙티브 디버깅 템플릿은 다음과 같은 항목으로 구성되어 있습니다:

1. 문제정의
2. 올바른 동작 정의
3. 최소 재현 환경 구축 및 관찰
4. 차이를 발생시키는 다양한 원인탐색
5. 가설 설정 및 검증

마지막 토론에선 강의에서 배운 디버깅 템플릿을 어디에 적용해 보면 좋을지에 대해 서로 이야기를 나눴습니다.

저는 문제들로 즐비한 오픈소스 Issue에 적용해 보면 좋을것 같다고 이야기했습니다.

며칠 후 @vitest/browser 테스트 라이브러리를 설정하는 도중 버그를 발견하게 되었고, 해당 문제 해결 과정에 템플릿을 적용해 보았습니다.

버그를 발견한 과정

먼저 @vitest/browser를 사용하게 된 배경을 말씀드리면 좋을것 같습니다. 저는 최근 scroll-wizard라는 스크롤 관련 문제를 해결하는 라이브러리를 제작하고 있습니다.

첫 번째 미션으로 모달과 같은 overlay를 띄울 때 overlay 하위 레이어에 스크롤을 방지하고자 document.body에 overflow 속성에 hidden을 주곤 하는데, 이 때 layout shift가 발생하는것을 해결하고자 했습니다. 코드는 간단했습니다. 하지만 라이브러리화를 한다고 했을 때 테스트코드가 없다면 사용하는 사람 즉 유저에게 신뢰를 주기 어렵다고 생각했습니다. 그래서 테스트코드를 작성해야만 했습니다.

스크롤은 브라우저에서 동작하므로 브라우저 모킹이 필요했습니다.

첫 번째로는 jsdom을 활용해 보았습니다. 그런데 jsdom은 스크롤 계산에 필요한 offsetWidth, clientWidth가 브라우저의 동작과 상이했습니다.

두 번째로는 playwright를 사용해보았으나, 정상 동작 하지 않았습니다.

test("If there is a scroll area, offsetWidth !== clientWidth", async ({
  page,
}) => {
  await page.setContent(`
      <style>
        .scroll-container {
          width: 200px;
          height: 100px;
          overflow: auto;

          padding: 20px;
          margin: 50px;
        }
        .children {
          height: 300px;
          background-color: orange;
        }
      </style>
      <div class="scroll-container">
        <div class="children"></div>
      </div>
    `);

  const obj = await page.evaluate(() => {
    function getElementWidth(selector: string) {
      const element: HTMLElement = document.querySelector(selector);

      return {
        offsetWidth: element.offsetWidth,
        clientWidth: element.clientWidth,
      };
    }

    return getElementWidth(".scroll-container");
  });

  console.log(obj.clientWidth, obj.offsetWidth);

  expect(obj.clientWidth !== obj.offsetWidth).toBeTruthy();
});

그렇게 감을 못잡고 있던 상황에서 최근 튜링의 사과에서 김태희님의 '테스트와 함께 프론트엔드 개발하기' 강연을 듣게 되었습니다.

강연중 @vitest/browser를 언급하셨고 힌트를 얻어, 사용해보기로 했습니다.

우선 라이브러리에 작성하기 전 정상적으로 모킹이 되는지 POC(Proof of concept)를 해보았습니다.

그 결과 사용하는 window API 들이 실제 브러우저와 동일하게 동작하는것을 확인 했고, 도입하기로 결정 했습니다.

그런데 실제로 프로젝트에 환경을 세팅하니 에러가 발생했습니다.

나의 문제? 우리의 문제?

POC 과정에서 정상 동작하는것을 확인 했기에 제가 설정한 모노레포 환경과 @vitest/browser의 호환을 문제로 꼽았습니다. 즉 '나의 문제로 정의' 했습니다.

// 문제 정의
내가 설정한 모노레포 환경과 @vitest/browser 환경이 호환 되지 않는다.

// 올바른 동작
내가 설정한 모노레포 환경과 @vitest/browser 환경이 호환되어 테스트 실행시 정상 동작해야한다.

// 가설
yarn berry + monorepo로 구성된 최소 실행 환경에서는 @vitest/browser를 설정하고 테스트시 정상 동작 할 것이다.

가설을 검증하기 위해선 최소 실행 환경을 구축해야만 했습니다.

mkdir bug

cd ./bug

yarn set version berry

yarn -v

yarn init -w

mkdir ./packages/repo

cd ./packages/repo

yarn init

yarn add -D vitest

yarn exec vitest init browser // (all press Enter)

yarn test:browser

결과는 동일하게 실패했습니다.

그렇다면 어떤것이 문제일지 살펴보았습니다.

'POC 과정에선 동작 했으니, 모노레포 문제일까?'

저의 가설을 틀렸으므로 저만의 문제는 아니라고 생각했습니다.

Github issue에서 Monorepo를 키워드로 찾아보았습니다. 관련 이슈가 존재 했습니다.

오픈소스의 버그라는것이 거의 명확해진 시점이었고, 우리의 문제를 해결해 컨트리뷰터가 되는 달콤한 상상을 했습니다. 🥺

그래서 좀 더 살펴보기로 했습니다. 👀

우리의 문제 해결기

두 가지가 마음에 걸렸습니다.

첫 번째로는 에러 로그 입니다.

Error: @vitest/browser tried to access vite, but it isn't declared in its dependencies; this makes the require call ambiguous and unsound.

'접근을 할 수 없다.. 왜 접근을 못할까?' 질문에 대한 답변이 바로 떠오르지 않았습니다.

두 번째로는 '정말 최소 환경 구축을 한 걸까?'에 대한 확신이 없었습니다. 우선 좀 더 환경을 좁혀보기로 했습니다.

이전까지 yarn berry + monorepo 환경이었습니다. 여기서 monorepo를 제거해보기로 했습니다.

mkdir bug

cd ./bug

yarn init -2

yarn add -D vitest

yarn exec vitest init browser // (all press Enter)

yarn test:browser

아니 이럴수가.. 🫨 monorepo 문제가 아니었습니다.

yarn_pnp_환경에서의_테스트_실패

첫 번째, 두 번째 의문이 뒤섞이면서 문득 yarn pnp가 유령 의존성 문제를 해결 해준다라는 글을 읽었던것이 떠올랐습니다.

그렇다면 에러 메시지의 말이 이해가 됩니다.

nodeLinker를 pnp가 아닌, node-modules로 변경하여 의도적으로 유령 의존성 환경을 만들어 보았습니다.

yarn config set nodeLinker node-modules

예상한대로 결과는 정상적으로 동작했고, 문제를 'pnp 환경에서 @vitest/browser는 동작하지 않는다.'로 재정의 했습니다.

이제 문제를 해결할 차례입니다.

유령 의존성을 없애기 위해선 패키지에 의존성을 명시하면 될 것이라고 생각했습니다. 에러의 메시지에서 그렇게 써 있구요. 😅

그렇게 가설을 세우고 관찰을 했습니다.

// 가설
@vitest/browser에 vite 의존성을 명시하면 의존성을 찾을 수 없는 문제가 해결될 것이다.

// 관찰
- 실제로 의존성 명시가 안되어 있을까?
- 의존성 명시를 안하고 사용하고 있을까?

실제로 해당 패키지에서 사용되고 있으나, 의존성이 명시되어 있지 않았습니다.

가설을 검증해보고자 해보고자 해당 레포지토리를 포크했습니다.

package.json에 의존성을 추가하고 잠깐 생각해보았습니다.

"dependencies": {
    ...
    "vite": "workspace:*",
  },

'그런데 클론 받은 프로젝트를 어떻게 로컬에서 테스트하지?'

'테스트를 하지 않고 pr 올리기엔 무책임한걸?'

여기서 scroll-wizard에서 npm에 패키지를 배포하기 전 로컬에서 테스트 하던 방법이 생각났습니다.

'npm link를 통해 글로벌 npm 패키지로 심볼릭 링크를 생성하여 테스트 해보면 되겠구나!'

실제로 좀 전에 구축했던 yarn berry 최소 실행 환경에서 위에서 정상 동작하는것을 확인 할 수 있었습니다.

가설이 입증되었으니 pr을 날릴 준비가 되었습니다.

하지만 추가적인 문제가 있었습니다.

vitest 프로젝트는 패키지매니저로 pnpm을 사용하는데 pnpm install시 lockfile이 너무 많이 변경되는겁니다.

vitest 프로젝트에는 lock file을 가능한 변경하지 말라는 문구가 존재하여 문제를 해결하고 pr을 올렸어야 했습니다.

여기서 한 단계 더 깊이 파고들 것인가에 대한 선택의 기로에 놓였습니다.

Issue도 훌륭해

저는 issue를 올리는 결정을 했습니다. 다른 분들이 해결해주길 바라면서요.

오픈소스의 매력은 이슈를 제기하는 사람이 문제 해결까지 하지 않아도 된다는 것입니다. 물론 하면 좋고 멋진 경험이겠죠.

여기서 vercel의 개발자이신 강동윤 님의 포스팅 제목을 인용하고 싶습니다.

"비판에는 대책이 없어도 된다."

저는 이슈를 올리고 다른 분들이 해당 이슈를 어떻게 해결하는지 보고 싶기도 했습니다.

몇 시간 뒤 vitest 프로젝트의 멤버가 커밋을 하고 이슈를 닫았습니다.

궁금한 마음으로 커밋을 확인해 보았습니다.

...

...

...

유령의존성_문제해결_커밋

저는 @vitest/browser의 package.json에 vite 의존성을 추가할 줄 알았는데 전혀 다른 방식으로 해결하였습니다.

바로 'vite'로부터 import 해오는것을 'vitest/config'에서 import 해오는 것으로요.

vitest/config에서 vite에서 export한 값들과 동일한 값들을 export하고 있었습니다.

해당 프로젝트의 구조를 속속히 알고있는 멤버이기에 가능한 문제해결 방법이었습니다.

제가 만약 pr을 올렸다면 의존성을 추가했을 것이고 lockfile 변경에 대해서 혹은 import 위치 변경과 같은 대화가 이어져 조금은 지연 될 수 있었을 것 같습니다.

개인적으론 조금은 아쉽긴 했지만, 그래도 빠르게 issue만을 남기길 잘했다는 생각이 듭니다.

지금에서야 보이는 것

그때는 안보였지만 다 지나고 보니, 문제는 터미널에 첫 번째줄에 적혀 있었습니다.

Error: @vitest/browser tried to access vite, but it isn't declared in its dependencies;

더 많은 배경 지식을 알고 있었다면, 해당 터미널 에러를 보자마자 오픈소스의 package.json을 보고 의존성 명시된 것을 발견할 수 있지 않았을까 싶습니다. 😂

결론

이펙티브 디버깅 워크숍에서 처음에 동준님이 템플릿 항목들을 차례로 설명해주시면서 순서대로 할 필요는 없다는 말씀을 하셨습니다. 또한 실제로 휘동님께서는 순서를 파괴하시면서 디버깅 하시기도 하셨구요. 제가 생각했을 때 디버깅시 중요한 것은 손이 먼저 나가는것이 아닌, 문제를 문장으로 만들어 보는것이란 생각이 듭니다. 왜 그런 경험들 있잖아요. 내 문제는 잘 안풀리는데 남이 말하는 문제는 해결법이 떠 올랐던?? 이런 관점에서 템플릿을 작성하는 것이 도움이 되는것 같습니다. 또한 TDD의 부산물인 테스트코드와 같이, 디버깅시 작성한 템플릿이 남게 되어 지금과 같이 포스팅을 통해 회고하는데 도움이 되는것 같습니다. 앞으로 더 많이 활용해보며 저만의 이펙티브 디버깅 템플릿을 깎아 나가야 겠습니다.