하나의 기능을 개발할 때, 프론트엔드 개발자의 할일을 아주 거칠고 단순한 단계로 나눠본다면 UI 개발
과 API 연동
두 가지일 것이다. 여기서 UI 개발
은 프론트엔드 개발자의 독자적인 영역으로 다른 팀이나 외부 인원의 개입 없이 적절한 디자인 가이드 또는 프로토 타입, 요구사항 정의만으로 진행할 수 있지만, API 연동
은 API라는 선제 조건이 있기 때문에 병목이 생기기 쉬운 단계다.
❓ UI 개발보다 API 연동을 먼저 해야 하는 이유
이전에는 아직 API가 개발되지 않은 상태에서 개발을 진행할 때, API가 없다는 이유로 API 요청 없이 dummy data(임의로 만든 JSON 파일)로 컴포넌트만 미리 만들어두곤 했다. 즉, API 연동에 앞서 UI 개발을 먼저 한 것인데, 많은 경우 이렇게 개발하면 아래와 같은 상황에서 개발 공수가 늘어났다.
- 데이터 상태에 따른 UI를 적용해야 한다
-
API 요청을 통해 데이터를 받아와서 뿌려주는 컴포넌트는
loading
이나error
등의 데이터 상태 객체를 활용해야 하는 경우가 대부분이다. 로딩 중에는 스켈레톤 UI를, 에러 응답의 경우 상황에 맞는 에러 컴포넌트를 보여주어야 하기 때문이다.{ loading && <Skeleton /> }
dummy data로 UI 개발을 먼저 진행할 경우, API가 개발되고 나서야 데이터 상태에 따른 UI 배리에이션을 적용할 수 있다. UI 개발을 먼저 진행해도, API가 개발될 때까지는 특정 UI에 대한 테스트를 미루어야 하는 것이다.
-
- 무한 스크롤 등 API 요청과 결합된 기능이 있다
- 마찬가지로 UI를 먼저 개발할 경우, 특정 영역에 도달하면 API 요청을 통해 추가적인 UI를 로드하는 무한 스크롤 등의 기능의 개발이나 테스트 또한 API 개발 이후로 미루어야 한다.
프론트엔드 개발은 눈으로 보이는 영역의 모양새뿐만 아니라, 유저와 서비스의 접점에서 상호작용하는 모든 것을 총괄한다. 컨텐츠를 불러오고, 유저의 입력값을 넘겨주고, 행동에 따라 유저 경험을 차별화 하기 위해서는 API request, response 데이터와 UI가 무 자르듯 나뉘어서는 안 되고 물 흐르듯 유연하게 결합되어 흘러가야 한다. ‘UI 개발 먼저 하고 API 연동해야지’가 언제나 한계에 부딪히는 이유다.
🦜 Next.js에 MSW 적용하기
마치 API가 개발된 것처럼 API 요청 코드를 자유롭게 작성하되, 실제 API 응답을 받은 것처럼 UI 개발이 가능케 하는 솔루션 중 가장 광범위하게 사용되는 것은 MSW(Mock Service Worker)다. MSW를 사용하면 말 그대로 서버를 흉내내 클라이언트의 요청을 가로채 원하는 응답을 받을 수 있다.
Next.js에서 MSW를 적용하기 위해서는 몇 가지 세팅이 필요하다.
-
MSW와 관련된 소스들을 관리하기 편하게 src 디렉토리 내
mocks
디렉토리를 생성한다. -
가로채고 싶은 API 요청과 원하는 response에 대한 내용을
handlers.ts
에 작성한다.import { graphql, rest } from 'msw'; // API response로 넘겨주고 싶은 데이터 import { posts } from './dummies'; // graphql API일 경우, 가로채고 싶은 요청 경로 링크 const client = graphql.link('https://www.api.com'); export const handlers = [ // graphql API일 경우 client.query('posts', (req, res, ctx) => { return res( ctx.data({ posts, }) ); }), // REST API일 경우 rest.get( 'https://www.api.com/posts', (req, res, ctx) => { return res(ctx.status(200), ctx.json(posts)); } ), ];
-
browser.ts
와server.ts
에 해당 handler를 import해 셋업한 후 내보낸다// browser.ts import { setupWorker } from 'msw'; import { handlers } from './handlers'; export const worker = setupWorker(...handlers); // server.ts import { setupServer } from 'msw/node'; import { handlers } from './handlers'; export const server = setupServer(...handlers);
-
MSW를 초기화하는 함수를 작성한다.
// index.ts export async function initMocks() { if (typeof window === 'undefined') { const { server } = await import('./server'); server.listen(); } else { const { worker } = await import('./browser'); worker.start(); } }
-
환경변수에 따라 조건부로 MSW를 초기화하는 MSW 컴포넌트를 생성한다.
'use client'; import { type PropsWithChildren, useEffect, useState } from 'react'; const isMockingMode = process.env.NEXT_PUBLIC_API_MOCKING === 'enabled'; export const MSWComponent = ({ children }: PropsWithChildren) => { const [mswReady, setMSWReady] = useState(() => !isMockingMode); useEffect(() => { const init = async () => { if (isMockingMode) { const initMocks = await import('./index').then(res => res.initMocks); await initMocks(); setMSWReady(true); } }; if (!mswReady) { init(); } }, [mswReady]); if (!mswReady) { return null; } return <>{children}</>; };
-
루트 컴포넌트를 MSW 컴포넌트로 감싸준다
// _app.tsx <MSWComponent> <Component {...pageProps} /> </MSWComponent>
-
MSW 관련 환경변수를 추가하고 동작을 확인한다
// .env NEXT_PUBLIC_API_MOCKING=enabled
*SSR을 하지 않는 리액트 프로젝트의 경우, server.ts
작성이나 initMocks
의 조건문은 제외할 수 있다.
🙀 개발된 API가 예상과 다를 때
이렇게 API 개발 일정과 관련없이 자유롭게 API를 모킹해 프론트엔드 개발을 완료했음에도, 개발된 API의 필드명이나 구조 등이 예상과 다른 경우도 왕왕 있다. 백엔드 개발자와 긴밀히 협의하고 개발을 시작하는 것이 베스트겠지만, 불가피한 사정으로 예상치 못한 API가 배포된 경우 그에 맞춰 UI 컴포넌트의 구조를 바꾸어야 할까?
그렇게 된다면 MSW를 사용해 미리 API 요청과 응답을 모킹한 보람이 사라지는 셈이다. 내가 참고한 카카오 엔터테인먼트 FE팀의 경우, BFF와 유사한 구조의 레이어를 추가해 API에 대한 디펜던시를 제거하는 방법을 제시한다. 서버 응답에 따라 변경이 필요한 로직은 factory라는 이름의 레이어를 거치도록 하는 것이다.
MSW를 사용하고 나서 API의 실제 개발 상황이 프론트엔드 개발에 미치는 영향이 미미해지면서 개발 경험(DX)뿐만 아니라 개발 속도도 현저히 향상되었다. 기존 UI 개발 → API 개발 → API에 의존하는 프론트엔드 개발
이었던 플로우에서 병목이 될 수 있는 API 개발이 끝으로 옮겨지면서 프론트엔드 개발의 흐름이 끊기지 않을 수 있게 되었기 때문이다. 물론 우리 개발 조직의 훌륭한 백엔드 개발자분들 덕분에 MSW를 전혀 사용하지 않고 개발한 경우가 더 많았지만, 앞으로도 ‘API가 안 나와서 못하고 있다’
는 책임 전가를 하지 않고 프론트엔드 개발을 완료할 수 있도록 적극적으로 MSW를 활용할 생각이다.