Storybook에 Mock Service Worker 적용하기

pds·2023년 6월 12일
1

TIL

목록 보기
60/60

StorybookMSW를 적용해보자


MSW

Service Worker를 사용하여 네트워크 호출을 가로채는 API 모킹 라이브러리

브라우저 상에서 백그라운드로 동작하여 지정된 경로의 API 호출을 시도할 때 이를 가로채서 지정된 가짜 응답을 반환하게 해준다.


왜 사용했나요??

클라이언트 개발 시 서버의 자원이 필요한 곳에서는 서버가 없다면 기능, UI를 개발할 수 없고 개발용 서버가 있다고 해도 개발 모드를 켜서 매번 요청해보고 응답을 확인하며 UI와 기능을 개발하는 것은 꽤 지쳤던 것 같다.

결국 서버 API의 존재 그 자체나 구현상태에 의존하게 되는데 사전에 정의된 API 명세를 확인하며 MSW로 서버 API를 모킹하고 테스트 코드 기반으로 개발을 진행하여 의존도를 줄이고 하고자하는 작업을 진행했다.

그리고 개발 서버에 구현되어있는 API를 활용해서 에러상황을 직접 만들어가며 개발하기에는 꽤 어려움이 있었던 것 같았는데 MSW를 사용해 테스트 기반으로 개발하면 이런 부분들에 대해서 사전에 핸들링 해보고 검증할 수 있게 된다.

  it('로그인 사용자일 경우 navigation에 프로필이 식별된다.', async () => {
    implementServer([
      restHandler(mockedGetUserWithServerApi),
    ]);
    renderQuery(<Navigation />);
    await waitFor(() => {
      expect(
        screen.queryByRole('button', {
          name: '로그인',
        }),
      ).not.toBeInTheDocument();
    });
    const imageAltText = await screen.findByAltText(/pds0309-avatar/i);
    expect(imageAltText).toBeInTheDocument();
  });

  it('로그인된 사용자가 회원 정보 조회에 실패할 경우 메인페이지로 이동하며 비로그인 사용자 UI가 식별된다.', async () => {
    resetServer([
      restHandler(mockedGetUserWithServerApi, { status: 400, message: 'BAD_REQUEST' }),
    ]);
    renderQuery(<Navigation />);
    await waitFor(() => expect(mockRouter.pathname).toEqual('/'));
    expect(screen.getByRole('button', { name: '로그인' })).toBeInTheDocument();
  });

react-testing-library를 사용해 백엔드 서버의 상황에 상관없이 API를 활용해 컴포넌트 또는 훅을 테스트해보고 assertion 할 수 있다.

완벽하지 않기 때문에 테스트 케이스에 비해 실제 환경에서 의도적으로 동작하지 않은 적도 있고 정말 모든 상황에 대한 테스트 케이스를 만들어 검증한 것은 아니기에 어느정도 (내 수준에는) 한계가 있지만

해보니 클라이언트 개발에 있어 필수라는 생각이 들었고 매우 도움이 되었다.

무엇보다 테스트케이스가 적절히 잘 작성된 경우에 대해서는 리팩터링이나 기능 추가/수정 시 매우 편리하고 사이드이펙트가 발생하지 않음을 어느정도 보장할 수 있었음이 편했다.


Storybook에 적용하게된 이유

개발 시 atomic 디자인 방식으로 진행했는데 atoms, molcules 같이 작고 역할이 제한적이며 비즈니스 로직이 결합되지 않은 부분들에 대해서는 스토리북 작성을 위주로 컴포넌트를 만들고 붙여나가는 방식을 사용했고,

organism, page와 같은 여러 컴포넌트가 결합된 복잡한 구조를 지녔고, api crud 같은 비즈니스 로직으로 동작하는 부분에 대해서는 jest, react-testing-library를 활용해 UI를 그려보기보다는 로직과 이벤트가 동작하는 것에 대한 컴포넌트 테스트를 진행하며 개발했다.

하지만 점점 아토믹 디자인 기준을 나누기 어려워졌고 atom, molcules를 재사용해 구성되는 organism보단 색다른 컴포넌트들이 늘어갔고 UI를 확인하지 않고 개발하기 어려워져 스토리 작성이 필요해졌다.

molcules까진 스토리를 작성하는데 전혀 문제가 없지만 organism 부터는 좀 문제가 있었다.

직접적으로 서버 Api를 사용해 처리하는 동작이 포함되어있어 스토리 작성으로 상호작용을 확인할 수 없거나

포함되어있지 않아도 페이지에서 props로 얻는 서버 데이터와 밀접하게 관련되어 동작하다보니 스토리 작성 자체가 매우 번거로워지는 문제가 생겼다.


수정,삭제와 같이 api가 의존된 컴포넌트의 스토리에서는 당연히 상호작용도 확인할 수 없다. 수정하기를 클릭해도 존재하지 않는 서버로 요청을 해 에러만 계속 식별될 뿐이다!

msw를 활용한다면 이런 부분에 대해서 실제 상호작용을 보여주는 것이 그렇게 어렵지 않아보여 적용했다.


MSW 연동하기

기존 jest환경은 msw/node(노드서버)를 사용해 가로챘는데

스토리북은 브라우저 환경이기 때문에 worker 설정을 해야한다.

공식문서에 있는 그대로 설치하고 설정하면 된다!


mockServiceWorker 생성

// /public 경로에 mockServiceWorker.js 파일 생성
npx msw init public/ --save 

worker 설정

// worker.js
import { setupWorker } from 'msw';

export const worker = setupWorker();

export const implementWorker = (restHandlers) => {
  worker.use(...restHandlers);
};

export const resetWorker = (restHandlers) => {
  worker.resetHandlers(...restHandlers);
};

위의 implementWorker 등등의 함수는 공식문서에 있는 것은 아니고

테스트케이스에서 필요한 가짜 api들(restHandlers)를 쉽게 정의해서 사용하려고 개인적으로 만들었다

사실 이전에는 필요한 api restHandler들을 모두 정의해두고 msw를 실행할 때 자동으로 등록하여 사용할 수 있는 방식이었다.

const worker = setupWorker(...handlers);

에러 리턴 등 다양한 상황에서 테스트케이스에 직접 server.use 또는 server.resetHandlers를 사용해 다시 등록하게되는 와중

어떤 테스트 케이스(사전에 설정된 핸들러를 사용하는 상황)에서는 이미 등록되어있는 걸 사용하다보니 일관성없이 중구난방이고 테스트를 읽을 때 어떤 api요청을 가로채 사용했는지 알기가 어려워

가독성을 높이고자 초기 msw 실행 중일 때는 아무 핸들러도 등록하지 않고 테스트케이스에서 직접 사용하게끔 변경했었다.


스토리북에서 worker 실행

// .storybook/preview.js
import { worker } from '../__tests__/__mocks__/msw/worker';

if (typeof global.process === 'undefined') {
  worker.start();
}

스토리북에서 MSW 실행중인지 확인하기


스토리 하나에 api mocking 적용 예시

export default {
  title: 'components/user/UserProfileHeader',
  component: UserProfileHeader,
} as ComponentMeta<typeof UserProfileHeader>;

const Template: ComponentStory<typeof UserProfileHeader> = () => {
  implementWorker([
    restHandler(() =>
      mockedGetUserWithServerApi({
        ...MOCK_MEMBER,
        name: '박동섞',
        profile: 'IMAGE_URL',
      }),
    ),
    restHandler(mockedPutUserApi),
  ]);
  const { user } = useUser();
  if (!user) {
    return <LoadSpinner width="100%" height="200px" />;
  }
  return <UserProfileHeader {...user} />;
};

export const Default = Template.bind({});

해당 컴포넌트 자체는 상위 페이지에서 props로 user 정보를 받아 UI를 구성하기 때문에 스토리 작성에 상태가 필요없지만

프로필 수정 상호작용 확인을 위해 mock api를 사용하면서 상태를 불러와 스토리를 작성해보았다.

업데이트 api를 msw가 가로채서 수정이 성공하고 그에 맞게 기존 상태가 잘 변경된다.



References

profile
강해지고 싶은 주니어 프론트엔드 개발자

0개의 댓글