React-Test-Library도입...Mock

minseok baek·2024년 7월 22일

프로젝트

목록 보기
8/20

React-Test-Library 도입

FSD 아키텍처 도입을 위해 순차적인 리팩토링을 진행하는 과정에서, 매번 작업 후 수동으로 테스트하여 문제를 파악하는 과정이 번거롭고 불편하다는 생각이 들었다. 또한, 작은 변화가 예상치 못한 문제를 일으킬 수 있다는 불안감이 생겼다. 좀 더 확신을 가지고 리팩토링을 진행하기 위해 테스트 코드를 작성하기로 결심했다.

테스트 코드 작성

테스트 코드는 BDD(Behavior-Driven Development) 형태의 describe-context-it 패턴을 사용하여 시나리오 방식으로 작성했다. 이렇게 작성하면 누구든지 코드를 접했을 때, 코드를 직접 파악하지 않아도 문서화처럼 어떤 기능을 하는지 명확히 알 수 있다. 또한, FSD 아키텍처가 작은 피처 단위로 구분하는 디렉토리 구조이기 때문에 유닛 테스트를 작성하기에도 매우 적합한 구조라고 생각했다. 특히나 공용적으로 많이 사용하는 shared 레이아웃 부분만큼은 리팩토링의 확신성을 보장받을 수 있는 증명이 필요하다고 생각했다. 많은 레이어에서 사용하는 만큼 작은 변화로 인한 오류도 치명적일 수 있기 때문이다.

Mock..

테스트 코드를 작성하면서 가장 머리 아팠던 부분은 바로 목킹(mocking)이다. 목킹을 위한 코드가 클래스 형태인지, 함수 형태인지, default export인지, named export인지, 웹 API인지에 따라 목킹 방식이 다양하다는 점이 정말 어려웠다. 그러나 코드의 의존성을 단위 테스트에서 효과적으로 격리하기 위해 목킹은 필수적인 과정이다.

Default Export와 Named Export의 Mocking 문제

처음 겪었던 문제는 모듈을 default export 방식으로 내보낸 코드와 named export 방식으로 내보낸 코드의 목킹 방식이 다르다는 것이었다. 이 부분에서는 코드에서 딱히 오류 메시지도 방출하지 않고, 특별히 큰 문제임을 확인할 수 없는 점이 가장 난감했다.


// Mocking default export
jest.mock('../text', () => ({
  default: jest.fn(() => 'Mocked default export')
}));
// Mocking named export
jest.mock('../text', () => ({
  TestMock: jest.fn(() => 'Mocked named export')
}));

외부 주입 클래스의 경우 Jest.spyOn

다음으로 겪은 문제는 로컬 스토리지를 좀 더 간편하게 사용하기 위해 서비스 목적의 유틸리티 클래스를 만들어 Theme 설정의 훅의 의존성으로 연결한 부분이었다. 클래스의 인스턴스를 훅 내부에서 생성하지 않고 훅 외부에서 생성한 이유는 훅 내부에서 생성할 경우 클래스 인스턴스를 훅의 렌더링 시마다 생성하게 되어 메모리 낭비가 발생하기 때문이다. 외부에서 주입받는 형태로 작성하여 이러한 문제를 해결했다.

하지만 이 경우 테스트 코드를 작성할 때 일반적인 목킹을 사용하면 생성된 인스턴스에 접근하지 못하는 현상을 발견했다. 이러한 경우에는 클래스를 목킹하기 위해 Jest.spyOn을 활용하여 외부에서 생성된 클래스의 동작을 추적하고 조작해야 한다.

 let getMock: jest.SpyInstance;
 let saveMock: jest.SpyInstance;

    getMock = jest
      .spyOn(LocalStorageManager.prototype, 'get')
      .mockImplementation();
    saveMock = jest
      .spyOn(LocalStorageManager.prototype, 'save')
      .mockImplementation();

WEP API의 목킹은 window.API

MediaStream과 같은 웹 API를 사용하는 경우에는 MediaStream과 유사한 클래스를 직접 생성하거나, window.[API] = jest.fn().mockImplementation(() => 유사한 객체)의 값을 반환해주는 형태로 목킹해야 한다. 얼핏 보면 정말 간단한 문제 같지만, WEB API를 목킹하려면 해당 웹 API의 개념을 기본적으로 이해하고 있어야 API 메서드를 제대로 컨트롤할 수 있다는 점이 있다.

오버플로우를 찾아본 결과, 직접 클래스를 만들어주는 형태로 사용하는 사람들도 있었다. 하지만 이 경우는 정말 해당 API에 대해 깊은 이해를 하고 있어야 가능하고, 어설프게 구현하면 문제가 복잡해질 확률이 높다. 확실히 활용성 측면에서는 다양하겠지만, 극한의 난이도에 비해 지금 해결해야 할 문제에서는 크게 이점이 없었다. 그래서 직접 해당 메서드의 값을 반환해주는 형태로 작성했다.

    mockStream = {
      getAudioTracks: jest.fn().mockReturnValue([{ stop: jest.fn() }]),
      getVideoTracks: jest.fn().mockReturnValue([{ stop: jest.fn() }]),
    };

    Object.defineProperty(navigator, 'mediaDevices', {
      writable: true,
      value: {
        getUserMedia: jest.fn().mockResolvedValue(mockStream),
      },
    });

경험 및 결론

아직 전체적인 테스트 코드 과제와 FSD 아키텍처 전환을 완전히 해결하지는 못했지만, Shared Layer의 경우 우선적으로 디렉토리 구조를 옮기고, 테스트 코드 작성까지 마무리한 상태이다. 여전히 단점은 FSD 아키텍처에 대해 생소한 팀원이 처음 접하면 "이게 뭐지? 왜 쓸데없이 디렉토리를 많이 만들었을까?"라는 생각이 들 수 있다는 것이다. 하지만 FSD의 개념을 조금 이해하고 리팩토링과 같은 보수 작업을 경험하다 보면 정말 편리하다는 것을 느낄 것이다.

테스트 코드를 작성하면서 느낀 점은, 아직 프로젝트의 일부 목킹 유형만 접했을 뿐인데도 매우 어려웠다는 것이다. 솔직히 지금은 문제를 해결하고 글로 나열하는 시점이지만, 하나의 문제 유형을 해결하기 위해 기본적으로 5시간 이상을 투자한 것 같다. 무엇보다 해결해야 했던 문제들이 일반적인 대중적인 문제가 아니어서 예시도 없고 자료도 부족했다. Jest 공식 문서의 Mock 파트를 전체 한글 번역하고, 학습하고, Stack Overflow를 돌아다니면서 비슷한 유형의 문제들을 탐색하며 문제를 해결했다. 아직 갈 길이 한참 멀다는 것을 또 한 번 느끼는 순간이었지만, 이번 주에도 새로운 성장을 이룬 기분이다. ㅎㅎ

단언컨대, FSD 아키텍처와 테스트 코드는 정말 환상의 조합이다. 테스트 코드를 작성할 때는 관심사와 복잡한 의존성이 엮일 경우 정말 난감해진다. 하지만 FSD 아키텍처는 피처 단위로 구조를 분할하기 때문에 원칙을 확실히 지킨다면 복잡한 관심사나 의존성에 얽힐 문제가 전혀 없다. 또한 레이어의 단계별로 올라가기 때문에 레이어의 최하층 테스트 코드를 작성하는 것만으로도 의존성 문제나 버그를 최소화할 수 있다.

profile
성장은 점진적 과부하, 매주 회고를 목표로 시작했지만 그때 그때 컨셉이 달라요. 시행착오를 통해 저만의 방식을 찾아가는중입니다.

0개의 댓글