장바구니 미션에서는 총 두가지 페이지(상품 목록 페이지, 장바구니 페이지)를 구현해야 하고 애플리케이션 전체에서 사용되는 전역 상태를 관리해야 한다.
MSW란, Mock Service Worker의 약자로, 서비스 워커(Service Worker)를 사용하여 네트워크 호출을 가로채는 API 모킹 라이브러리이다. 즉, MSW를 사용하면 따로 백엔드 서버가 없어도 브라우저 상에서 http 요청을 가로채서 서버가 존재하는 것 마냥 동작시킬 수 있다.
백엔드와의 협업을 진행하기 앞서 MSW로 mock API를 직접 제작해보는 경험을 했다.
먼저, MSW 패키지를 먼저 설치해준다.
npm i -D msw
패키지가 설치 됐다면, 다음과 같이 msw 기반 코드를 생성해준다.
npx msw init public/ --save
msw init을 할 때 프로젝트의 public directory를 지정해줘야 하는데, 일반적으로 public/
경로에 public directory가 존재하므로 위와 같은 명령어로 기반 코드를 생성해주었다.
// src/mocks/handlers.ts
import { rest } from "msw";
const cartList = [
{
id: '0',
quantity: 1,
product: {
id: 0,
name: '순살치킨 1KG',
price: 9900,
imageSrc:
'https://cdn-mart.baemin.com/sellergoods/main/c6f2f083-a8b8-4799-834b-444b5eaeb532.png?h=400&w=400',
},
},
...
];
export const handlers = [
rest.get('/cart-items', (req, res, ctx) => {
return res(ctx.status(200), ctx.json(cartList));
}),
];
/cart-items
url로 get 요청을 받으면 상태 코드 200과 함께 장바구니 목록 데이터를 응답으로 보내준다.
export const handlers = [
...,
rest.post<{productId: number}>(
'/cart-items',
async (req, res, ctx) => {
const { productId } = await req.json();
// request body로 받은 productId에 해당하는 상품을 찾아서 product에 저장
const product = mockProducts.find((product) => product.id === productId);
// productId에 해당하는 상품이 없다면 500 에러 발생
if (!product) {
return res(ctx.status(500));
}
const newCartItem = {
id: uuid(),
quantity: 1,
product,
};
cartList.push(newCartItem);
return res(ctx.status(201));
},
),
]
/cart-items
url로 productId
값을 담은 body와 함께 post 요청을 받으면, productId에 해당하는 상품을 찾고, 해당 하는 상품이 없다면 500 상태 코드를 응답한다. 해당하는 상품이 있다면 새로운 장바구니 아이템 데이터를 만들어서 cartList 배열에 push 해준 뒤 201 상태 코드를 응답해준다.
위와 같이 만든 핸들러 코드를 가져와서 서비스 워커를 다음과 같이 생성한다.
import { setupWorker } from "msw";
import { handlers } from "./handlers";
export const worker = setupWorker(...handlers);
서비스 워커를 생성했다면 애플리케이션의 진입 지점인 index.tsx
에 서비스 워커를 실행시켜주는 코드를 작성한다.
// src/index.tsx
import { worker } from "./mocks/worker";
if (process.env.NODE_ENV === "development") {
worker.start();
}
...
worker의 start
함수로 worker를 실행해준다. 이 때, 일반적으로 development 환경에서만 mock API를 사용하므로 NODE_ENV가 ‘development’ 일 때만 실행 시켜주도록 한다.
MSW를 사용해 장바구니 관련 데이터를 주고 받는 API들을 모킹했다. MSW로 만든 mock API 요청들은 실패없이 잘 작동하여 데이터를 주고 받는다. 그러나 실제 환경에서도 MSW를 사용한 것 처럼 모든 통신이 순조롭게 잘 작동할까? 실제 환경에서는 네트워크 환경이 열악할 수도 있고, 예상하지 못한 데이터를 받을 수도 있다. MSW를 단순 데이터를 주고 받는 것 그 이상으로, 더 ‘잘’ 쓰기 위해선 어떻게 할 수 있을까?
바로 실제 상황에서 발생 할 수 있는 다양한 경우의 수를 스스로 시뮬레이션
해보는 것이다. 각 요청마다 발생할 수 있는 여러 상황들을 나열하고, 해당 상황에 대한 MSW API 코드로 구현한 뒤 직접 시뮬레이션 해본다면 MSW 코드 뿐만 아니라 애플리케이션 코드도 더욱 단단하고 퀄리티 있는 코드 작성을 할 수 있다.
예를 들어 각 요청 별 다음과 같은 상황이 있을 수 있다.
위와 같이 여러 상황들을 나열한다면 실제 애플리케이션에서 발생할 수 있는 예외도 쉽게 발생할 수 있고, 해당 상황들을 메꾸면서 빈틈 없는 애플리케이션을 만들어 나갈 수 있다. 다음은 위에서 제시한 몇 가지 예시들을 실제로 다음과 같이 시뮬레이션 했다.
rest.get('/product-items', (req, res, ctx) => {
return res(ctx.status(200), ctx.json(mockProducts), ctx.delay(3000));
})
MSW의 ctx에는 다양한 유틸 함수들을 지원하는데, 그 중에서 delay라는 함수는 해당 api 응답을 원하는 시간만큼 지연할 수 있다. 위 코드에서는 상품 목록 GET 요청에 대한 응답을 3초 뒤에 해주도록 설정해줬다.
예상한 상황에 맞게 MSW를 재현했고, 이를 실제 애플리케이션에서 시뮬레이션 해보니 상품 목롤 요청을 기다리는 동안 아무런 피드백이 없으니 좋지 않은 사용자 경험이라는 것을 깨달았다. 이러한 문제를 대응하기 위해 상품 목록이 불러와지는 동안 보여줄 스켈레톤 UI를 적용시켜줬다.
장바구니 아이템 추가 POST 요청 코드를 보면, 이미 상품 id가 존재하지 않는 경우에 대한 예외 처리는 500 상태 코드 응답으로 해준 상태이다. 그러나 단순 500 에러로는 api 사용처에서 어떤 에러인지 명확하게 구분하기가 힘들다. 다음과 같이 코드를 변경했다.
rest.post<{productId: number}>(
'/cart-items',
async (req, res, ctx) => {
const { productId } = await req.json();
const product = mockProducts.find((product) => product.id === productId);
// 바뀐 부분
if (!product) {
return res(ctx.status(400), ctx.json({message: '해당하는 상품이 존재하지 않습니다.'}));
}
const newCartItem = {
id: uuid(),
quantity: 1,
product,
};
cartList.push(newCartItem);
return res(ctx.status(201));
},
기존 500 상태 코드만 가지고 있었던 응답을 400 코드와 응답 body의 message로 해당 에러에 대해 더 명확하게 명시해줬다. 이로 인해 API의 사용처에서 예외 처리를 더 쉽게 할 수 있다는 효과를 얻을 수 있었다.
MSW를 사용해보며 느낀 가장 큰 장점은 백엔드에 의존하지 않고 개발할 수 있다는 점이다. 백엔드 개발이 완료되지 않아도 미리 작성된 예상 명세를 토대로 mock API를 만들어두면 나중에 백엔드가 완료 되었을 때 base url만 살짝 바꿔주면 쉽게 적용이 가능하다. 그렇기 때문에 기존의 동기적이었던 통신 작업을 비동기적으로 작업 가능하므로 훨씬 협업 효율이 증가 할 수 있다. 또한, 클라이언트 입장에서 API를 만들어보는 작업이기 때문에 백엔드 개발자에게 클라이언트 시점으로 API에 대한 구체적인 피드백을 줄 수 있다는 장점도 있다.
실제 애플리케이션에서 발생할 수 있는 다양한 상황들을 MSW로 재현 해볼 수 있다는 장점이 있다. 어떤 데이터를 임의로 설정하고 싶거나 응답 속도 등을 임의로 정하고 싶을 때, 실제 API로는 이를 제어하기가 쉽지 않다. 이럴 때 MSW를 사용해서 다양한 상황을 시뮬레이션 해볼 수 있다. 다양한 예외 상황을 직접 시뮬레이션 해보고 실제 애플리케이션에서는 어떻게 작동되고 보여지는지 확인할 수 있다. 이로 인해 훨씬 더 효과적인 예외 처리 작업을 수행할 수 있기 때문에 더욱 단단하고 빈틈없는 애플리케이션을 완성할 수 있었고, MSW는 단순 백엔드 통신 대체용 도구 그 이상의 역할을 할 수 있었다.