본 시리즈는 정재남님의 풀스택 리액트 라이브코딩 - 간단한 쇼핑몰 만들기 강의 내용을 기반으로, 추가적인 학습을 통해 습득한 지식 또는 강의 코드를 다른 방법으로 구현한 경험을 작성하고 있습니다. 강의 코드(GitHub)를 확인하세요.
애플리케이션을 만들 때 프론트엔드는 백엔드의 API를 활용이 필수적이었다.
때문에 백엔드의 구현이 끝나고 프론트의 개발이 시작되면 가장 이상적이겠지만, 생산성 측면에서 시간이 너무 많이 소요되는 단점이 있다.
따라서 생산하는 데 소요되는 시간을 줄이기 위해 백엔드와 프론트의 개발이 동시에 진행되는 경우가 즐비한데, 이때 사용할 수 있는 방법이 Mocking이다.
위키 백과에서는 Mock의 의미를 아래와 같이 정의했다.
"모의 객체(Mock Object) 란 주로 객체 지향 프로그래밍으로 개발한 프로그램을 테스트 할 경우 테스트를 수행할 모듈과 연결되는 외부의 다른 서비스나 모듈들을 실제 사용하는 모듈을 사용하지 않고 실제의 모듈을 "흉내"내는 "가짜" 모듈을 작성하여 테스트의 효용성을 높이는데 사용하는 객체이다. 사용자 인터페이스(UI)나 데이터베이스 테스트 등과 같이 자동화된 테스트를 수행하기 어려운 때 널리 사용된다."
- 데이터베이스 테스트 : 자료의 변경을 수반하는 데이터베이스에 대한 작업을 테스트 하는 경우 테스트 수행 후 매번 데이터베이스의 자료를 원래대로 돌려놔야 하는데 이럴 경우 모의 객체를 이용해 데이터베이스의 응답을 흉내내어 데이터의 변경 없이 테스트가 가능하다.
즉, 프론트엔드의 Mocking이란 백엔드 API에 대한 의존성을 낮추고자 API가 필요한 시점에 실제 API가 아닌 모의 API를 구성하는 것이다.
화면에 필요한 데이터 상태를 내부 로직에 직접 Mocking 작성
Mock 서버를 별도로 구현
Mocking 라이브러리 사용
강의에서는 3번을 방법을 채택하여, MSW(Mock Service Worker)와 GraphQl을 사용해서 Mocking을 구현했다.
MSW는 Service Worker를 통해 서버의 실제 네트워크 요청을 가로채서 모의 응답(Mocked response)을 보내주는 API Mocking library로, Mock 서버를 구축하지 않아도 API를 네트워크 수준에서 Mocking 할 수 있다.
Service Worker는 웹 애플리케이션, 브라우저 및 네트워크(사용 가능한 경우) 사이에 있는 프록시 서버 역할
웹 애플리케이션의 메인 스레드와 분리된 별도의 백그라운드 스레드(worker context)에서 실행시킬 수 있는 기술로, DOM에 접근 권한이 없고 JavaScript와 다른 스레드에서 실행되어 UI Blocking 없이 연산을 처리할 수 있다.
graphql-tag, graphql-request 패키지를 사용하는 이유?graphql-tag: Apollo GraphQL 라이브러리에서 제공하는 패키지로, GraphQL 쿼리를 작성하는 데 사용된다. GraphQL 쿼리를 문자열로 작성하는 대신 태그된 템플릿 리터럴(gql)을 사용하여 쿼리를 작성할 수 있다. -> 코드 가독성을 높임graphql-request: 클라이언트에서 서버로 GraphQL 요청을 보내는 데 사용되는 패키지로, fetch API를 기반으로 작성되어 있으며 쿼리를 보내고 결과를 처리하는 데 사용되는 함수를 제공한다.두 패키지의 위와 같은 특징으로 graphql-tag와 graphql-request를 함께 사용하면 GraphQL 쿼리를 작성하고 서버에 요청을 보내는 데 필요한 모든 기능을 사용할 수 있다. 다른 패키지를 사용하면 추가적인 설정과 구성이 필요할 수 있으며 MSW와 호환성이 보장되지 않을 수 있다.
MSW는 GraphQL API에 대한 요청을 캡쳐하기 위한 Request handlers(query, mutation)와 utilities(operation, link)를 제공한다. 이를 이용하여 클라이언트에서 서버 API를 호출할 때 가짜 응답을 반환할 수 있다.
query(): GraphQL query에 대한 가짜 응답을 반환하는 응답 핸들러로, graphql.query(queryName, callbackFunction)으로 작성한다.mutation(): GraphQL mutation에 대한 가짜 응답을 반환하는 응답 핸들러로, graphql.mutation(queryName, callbackFunction)으로 작성한다.queryName: 메서드를 호출할 때 첫 번째 인수로, 서버에서 정의한 GraphQL query name 또는 mutation name과 일치해야 한다.callbackFunction: 메서드를 호출할 때 두 번째 인수로 전달되는 콜백 함수이다. 이 함수는 req, res, ctx 객체를 인자로 받아 (req, res, ctx) => { return res( /* 내용 */ ) }로 작성한다.req: 요청 객체로, GraphQL query 또는 mutation, 변수 및 기타 메타데이터를 포함한다.res: 응답 객체로, 클라이언트에 반환할 데이터를 설정하는 메서드를 포함한다.ctx: 컨텍스트 객체로, 응답 객체 및 요청 객체를 조작하는 데 사용한다.operation()query(), mutation() 등 모든 operation 타입을 처리할 수 있어 유연하다.graphql.operation(operationType, queryName, callbackFunction): 첫 번째 인수로 operationType을 추가 전달하는 것 외에는 다른 요청 핸들러와 작성 방법이 동일하다.// /src/mocks/handlers.tsx
import { graphql } from 'msw';
export const handlers = [
graphql.query('GET_PRODUCT', ({ variables }, res, ctx) => {
const product = mockProducts.find(product => product.id === variables.id);
if (!product)
return res(ctx.status(404), ctx.errors([getNotFoundError(variables.id)]));
return res(ctx.data({ product }));
}),
];
import { gql } from 'graphql-tag';
export const GET_PRODUCT = gql`
query GET_PRODUCT($id: ID!) {
product(id: $id) {
id
title
imageUrl
description
price
createdAt
}
}
`;
// /src/service/queries/products.ts
export interface ProductType {
id: string;
title: string;
imageUrl: string;
description: string;
price: number;
createdAt: number | null;
};
export const useGetProduct = (id: string) => {
const { data } = useQuery<{ product: ProductType }, ResponseError>({
queryKey: QueryKeys.PRODUCTS.product(id),
queryFn: () =>
request({
url: API_URL,
document: GET_PRODUCT,
variables: { id },
}),
});
return data ? { data: data.product } : { data: {} };
};