티스토리 뷰
Demo page: http://mock-pilot-demo.s3-website.ap-northeast-2.amazonaws.com/
github: https://github.com/kiwonbyun/mockpilot
개발 동기
내가 좋아하는 몇몇 라이브러리 소스코드를 보다 보니 라이브러리라고 대단한 게 아니고 나도 할 수 있겠다는 자신감이 들었다. 그래서 뭔가 만들어봐야겠다는 생각이 있었다. 하지만 내가 가장 싫어하는 것은 행동에 이유가 없는 것이다. 정체를 알 수 없는 뭔가(?) 개발하기 위해 행동할 순 없다. 개발에 나서기 위해서는 이유(해결하고자 하는 불편함)가 있어야 했다. 뜬금없이 예전에 읽었던 뉴타입의 시대라는 책이 떠올랐다. 문제를 해결하는 사람이 아니라 문제를 문제라고 말하고 정의하는 사람이 된 것 같았다.
내가 신입시절 첫 회사에서 겪었던 불편함은 프론트엔드 개발과 백엔드 api개발 사이에 pending기간이 생긴다는 것이었다. 보통 프론트엔드 개발자들이 화면을 만들고 나서 기능을 붙일 때 api와 연동하면서 인터렉션을 만드는데, api가 개발되기 전까지는 대기하는 시간이 생겼다. 나는 이 시간이 너무 아깝고 당장 해결이 필요한 문제라고 생각했다. 그래서 msw라이브러리를 사용하여 api를 서비스워커에서 모킹 하여 개발기간을 앞당긴 적이 있었다.
이직한 회사에서도 동일한 문제를 겪고 있었다. 아니 사실 문제라고 생각하지 않았다. 그냥 '원래 이런 거'라고 생각하는 상황이었다. 그래서 msw를 사용하면 기간을 앞당길 수 있다고 제안했지만 반응이 뜨뜻미지근했다. 말로는 안 했지만 문제는 안 그래도 공부해야 할 거 많은데 모킹 때문에 msw공식문서를 읽고 공부할 시간이 없다는 것이었다.
그럼 이 문제를 해결해 보자. api라는 게 뭔가? Application Programming Interface 아닌가? 왜 Interface가 필요한가? 내부에 복잡한 로직을 몰라서 사용자가 쉽게 사용할 수 있게 해 보자는 것이다. User Interface도 마찬가지이다. 유저가 서비스를 사용하기 쉽게 인터페이스를 제공해 보자는 것이다. 그럼 나는 msw를 사용할 수 있도록 내부의 복잡한 것들을 숨긴 인터페이스만 제공해 보자. msw를 사용하여 모킹을 쉽게 등록, 제어할 수 있는 UI를 만들어보자
중점
1. 개발자뿐만 아니라 기획팀, QA팀도 사용할 수 있도록 복잡함이 드러나지 않게 UI로 제공할 것
2. dev tools로 제공
결과
tanstack-query dev tools처럼 화면에 있는 아이콘을 누르면 오른쪽에 있는 Drawer가 나온다.
이곳에서 ui를 통해 원하는 api를 모킹 하면 된다.
예를 들어 위와 같이 등록할 수 있다. method, url, delay, status, response를 등록하고 Add Mock버튼을 누르면 된다.
네트워크 탭을 통해 service worker에서 되돌아오는 응답을 확인할 수 있다.
트러블슈팅
1. msw와 모킹객체를 연동하고 등록하는 코어역할을 하는 싱글톤 인스턴스와 UI의 상태를 동기화하기 위해 직접 모든 핸들러에서 상태 동기화 함수를 호출하는 불편함이 있었다.
const handleAddMock = () => {
mockPilotInstance.mock({...});
// mock이 추가된 후 수동으로 UI 업데이트를 해야 함
setMocks(mockPilotInstance.getMocks()); // 매번 이렇게 호출 필요
}
const handleDisableMock = () => {
mockPilotInstance.disable(id);
setMocks(mockPilotInstance.getMocks()); // 여기서도 필요
}
const handleRemoveMock = () => {
mockPilotInstance.remove(id);
setMocks(mockPilotInstance.getMocks()); // 여기서도 필요
}
예를 들면 위와 같은 모양인데, 저런 함수를 매번 다시 호출해야 하는 것이 불편했고, 실수할 가능성도 있었다.
전에 봤던 라이브러리 중에 subscribe라는 메서드를 만들어서 리액트 상태와 동기화하는 방식을 사용한 라이브러리를 본 적이 있다.
굳이 매번 리액트 상태를 업데이트해서 UI를 수동으로 동기화하지 말고 마운트와 동시에 subscribe를 호출해서 리액트 상태와 동기화된 함수를 만들면 어떨까?
class MockPilotImpl implements MockPilot {
...
private subscribers: Subscriber[];
constructor() {
...
this.subscribers = [];
}
subscribe(subscriber: (mocks: MockState[]) => void) {
this.subscribers.push(subscriber);
return () => {
const index = this.subscribers.indexOf(subscriber);
this.subscribers.splice(index, 1);
};
}
...
private updateWorker() {
this.worker.resetHandlers();
const handlers = Array.from(this.mocks.values())
.filter((mock) => mock.isActive)
.map((mock) => this.createHandler(mock));
this.worker.use(...handlers);
this.saveMocksToLocalStorage();
this.subscribers.forEach((listener) => {
listener(Array.from(this.mocks.values()));
});
}
}
모든 메서드들은 결국 updateWorker()를 호출하는데, 이 함수 내부에서 할 일을 모두 마친 후에 subscribers에 등록한 listener를 호출한다. listener는 현재 등록된 mocks를 인자로 호출된다.
이렇게 등록한 이유는 현재 등록된 mocks를 리액트 상태와 연동하기 위해서이다.
const initMockPilot = async () => {
if (process.env.NODE_ENV === "development") {
try {
const { mockPilot } = await import("../core/MockPilot");
mockPilot.subscribe((mocks) => setMocks(mocks));
await mockPilot.start();
mockPilotInstance.current = mockPilot;
setIsMount(true);
} catch (error) {
console.error("Failed to initialize MockPilot:", error);
}
} else {
setIsMount(true);
}
};
setMocks(mocks)에서 리액트 상태와 연동되므로 이제 mockPilot의 메서드를 호출하면 결국 리액트 상태와 연동되어 UI를 수동으로 업데이트할 필요가 없어진다.
2. Race Condition 문제
createRoot(document.getElementById("root")!).render(
<>
<App />
<MockPilotTools />
<>
);
MockPilotTools 가 마운트 후에 InitMockPilot 함수가 실행되고 동적으로 불러온 객체의 start() 메서드를 실행한다.
const initMockPilot = async () => {
if (process.env.NODE_ENV === "development") {
try {
const { mockPilot } = await import("../core/MockPilot");
mockPilot.subscribe((mocks) => setMocks(mocks));
await mockPilot.start();
mockPilotInstance.current = mockPilot;
setIsMount(true);
} catch (error) {
console.error("Failed to initialize MockPilot:", error);
}
} else {
setIsMount(true);
}
};
만약 App 마운트 즉시 실행되는 get요청이 모킹 되어 있다면 레이스 컨디션 문제가 발생하여 msw에서 제대로 응답을 가로채서 보내주지 못한다. 즉, NODE_ENV가 development일 때는 항상 mockPilot.start()가 먼저 실행되고 나서 App을 랜더링 해야 한다.
이를 구현하기 위해 App을 children으로 받아서 랜더링 할 수 있도록 구현하고 isMount가 true가 되고 나서 children을 랜더링 한다.
if (!isMount) {
return null;
}
return (
<>
{children}
이 외에도 스타일을 구현하면서 명시도 문제로 ui가 달라지는 등 여러 트러블슈팅을 했다.
계속 업데이트하면서 좋은 라이브러리가 되도록 만들어볼 예정이다.
'Javascript' 카테고리의 다른 글
ReadableStream으로 http요청 Streaming 하기 (2) | 2025.05.26 |
---|---|
clsx vs classnames. 라이브러리 분석 , 차이점 (1) | 2024.12.30 |
[Javascript]Binary Search Tree (0) | 2024.10.04 |
사칙연산 후위표기식의 계산 알고리즘 자바스크립트로 만들어보기 (1) | 2024.08.24 |
트러블슈팅: blob URL과 새 창에서 메모리 누수 방지하기! (2) | 2024.08.19 |