diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 273786c..1611753 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,3 @@ jobs: - name: Check prettier run: yarn check:prettier - - - name: Build - run: yarn build diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..a943536 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,26 @@ +name: DEPLOY + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 20.3.1 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install node packages + run: yarn + + - name: deploy + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global url.https://${{ secrets.PAT }}@github.com/.insteadOf https://github.com/ + yarn deploy-gh-prefix-paths \ No newline at end of file diff --git a/contents/posts/folder-structure/index.md b/contents/posts/folder-structure/index.md index 7e290fe..a095454 100644 --- a/contents/posts/folder-structure/index.md +++ b/contents/posts/folder-structure/index.md @@ -31,19 +31,17 @@ tags: - **apis** ![](apis-image.png) - - 백엔드 서버에 요청을 보내기 위한 api를 저장하기 위한 폴더입니다. - - 데이터와 관련된 요청을 위한 로직만 모여있기 때문에 services라는 이름 보다 apis가 더 적절하다고 생각해 이름을 변경했어요. + 백엔드 서버에 요청을 보내기 위한 api를 저장하기 위한 폴더입니다. 데이터와 관련된 요청을 위한 로직만 모여있기 때문에 services라는 이름 보다 apis가 더 적절하다고 생각해 이름을 변경했어요. - **libs** ![](libs-image.png) - - 외부 라이브러리와 관련된 파일들을 모아두기 위한 폴더입니다. - - text 가공을 위한 로직, 비디오를 다루기 위한 로직 등이 담겨있어요. + 외부 라이브러리와 관련된 파일들을 모아두기 위한 폴더입니다. text 가공을 위한 로직, 비디오를 다루기 위한 로직 등이 담겨있어요. - **utils** ![](utils-image.png) - - 그 외 프로젝트 전반에서 사용되는 잡다한 코드들을 위한 폴더입니다. - - 그로잉 프로젝트에서 사용하고 있는 react-query, dayjs, react-hook-form 등의 라이브러리를 사용하는 데 필요한 로직들이 담겨있어요. + + 그 외 프로젝트 전반에서 사용되는 잡다한 코드들을 위한 폴더입니다. 그로잉 프로젝트에서 사용하고 있는 react-query, dayjs, react-hook-form 등의 라이브러리를 사용하는 데 필요한 로직들이 담겨있어요. ### 2. types → models @@ -52,13 +50,11 @@ tags: - **models** ![](models-image.png) - - 기존에는 `types/chat/` 폴더의 하위에 `chatLine.dto.ts`, `chat.dto.ts` 등으로 하나의 dto를 하나의 파일에 작성했었는데 이를 기능에 따라 그룹화해 `models/` 에 위치 시켰습니다. - → 한 파일로 여러 dto들을 모아 두면 다른 파일에서 dto를 import할 때 import문이 간략해진다는 장점이 있어요! - - models 에는 백엔드와의 데이터 교환을 도와주는 dto들을 모아두었습니다. + 기존에는 `types/chat/` 폴더의 하위에 `chatLine.dto.ts`, `chat.dto.ts` 등으로 하나의 dto를 하나의 파일에 작성했었는데 이를 기능에 따라 그룹화해 `models/` 에 위치 시켰습니다. 이렇게 하면, 한 파일로 여러 dto들을 모아 두면 다른 파일에서 dto를 import할 때 import문이 간략해진다는 장점이 있어요. 그리고 models 에는 백엔드와의 데이터 교환을 도와주는 dto들을 모아두었습니다. - **types** ![](types-image.png) - - dto 외의 font, style을 위한 타입들을 위한 폴더입니다. + dto 외의 font, style을 위한 타입들을 위한 폴더입니다. 위와 같이 세부적으로 폴더를 분리해 로직의 역할에 따라 파일들을 분류할 수 있었습니다. 사실 dto 파일들은 api와 관련된 파일들이기 때문에 apis폴더 안에 위치시키는 것이 좋을 지에 대한 고민이 있었어요. 하지만 정의된 dto들이 apis폴더에 있는 파일 뿐 아니라 react-query를 위한 쿼리들이나 컴포넌트 파일에서도 사용되기 때문에 `src/models` 폴더에 위치시키기로 했습니다. @@ -153,11 +149,11 @@ export default Header; 그래서 저희는 파일 유형에 따라 분류를 했던 폴더 구조에서 도메인(기능)에 따라 분류를 하는 폴더 구조로 전환했습니다. - components 폴더에는 프로젝트에서 공통으로 사용할 컴포넌트를 모아둡니다. - ![](components-image.png) - 도메인별 페이지, 해당 도메인 내에서 사용되는 컴포넌트와 훅들은 모두 pages에 모아둡니다. - + + 위와 같이 폴더 구조를 변경한 후, 각 기능별 페이지 폴더 안에 필요한 파일들이 들어있는 형태가 되어 도메인별로 응집도를 높일 수 있었어요. @@ -187,9 +183,12 @@ import { Modal, AlbumModal } from "components/common" // 간단 그러나 베럴 파일 사용에는 몇 가지 고려해야 할 사항이 있다고 해요. 특히, 대규모 프로젝트의 경우 다음과 같은 이유로 주의가 필요합니다. 1. **트리 쉐이킹(Tree Shaking) 최적화** - - 웹팩(Webpack)과 같은 모듈 번들러는 사용되지 않는 코드를 제거하는 트리 쉐이킹 과정을 통해 최종 번들의 크기를 줄입니다. 베럴 파일을 통해 모든 모듈을 임포트하면, 실제로 사용되지 않는 모듈까지 번들에 포함될 가능성이 있어 트리 쉐이킹의 효율성이 떨어질 수 있습니다. + + 웹팩(Webpack)과 같은 모듈 번들러는 사용되지 않는 코드를 제거하는 트리 쉐이킹 과정을 통해 최종 번들의 크기를 줄입니다. 베럴 파일을 통해 모든 모듈을 임포트하면, 실제로 사용되지 않는 모듈까지 번들에 포함될 가능성이 있어 트리 쉐이킹의 효율성이 떨어질 수 있습니다. + 2. **코드 스플리팅(Code Splitting) 최적화** - - 베럴 파일을 사용하면, 필요한 컴포넌트만을 로드하는 대신 관련된 모든 컴포넌트가 함께 로드될 수 있습니다. 이는 초기 로딩 시간에 부정적인 영향을 줄 수 있으며, 특히 대규모 애플리케이션에서는 성능 저하의 원인이 될 수 있습니다. + + 베럴 파일을 사용하면, 필요한 컴포넌트만을 로드하는 대신 관련된 모든 컴포넌트가 함께 로드될 수 있습니다. 이는 초기 로딩 시간에 부정적인 영향을 줄 수 있으며, 특히 대규모 애플리케이션에서는 성능 저하의 원인이 될 수 있습니다. 이러한 이유로, 베럴 파일의 사용을 재고하고 프로젝트의 현재 방식을 유지하기로 결정했어요. diff --git a/contents/posts/msw-toolbar/index.md b/contents/posts/msw-toolbar/index.md index 8b5d6d9..8ddd199 100644 --- a/contents/posts/msw-toolbar/index.md +++ b/contents/posts/msw-toolbar/index.md @@ -1,426 +1,25 @@ --- -title: "MSW로 mocking을 해보자 " -description: "그로잉 팀의 MSW Toolbar 제작기를 소개할게요." -date: 2024-02-24 -update: 2024-02-24 +title: "API 요청 시뮬레이션을 간편하게: 그로잉 팀의 MSW Toolbar 도입기" +description: "프론트엔드에서 네트워크의 응답 시간을 조절하거나 특정 API 요청에서 에러 상태를 유발하는 등의 상황을 테스트하는 건 번거로운 일이에요. 이를 해결하기 위한 그로잉 팀의 MSW Toolbar 제작기를 소개할게요." +date: 2024-02-25 +update: 2024-02-25 tags: - growing - msw - mocking +series: "MSW Toolbar로 다양한 시나리오 테스트하기" --- -## mocking에 대해 알아보자 - -### **1. mocking 이란?** - -**모킹**은 특정 객체의 실제 구현 대신 가짜 객체를 사용해서 그 객체의 행동을 모방하는 프로세스입니다. 이러한 가짜 객체를 'mock 객체'라고 해요. Mock 객체는 우리의 서비스가 의존하고 있는 외부 시스템이나 복잡한 로직 없이도 동작할 수 있도록 해줍니다. 예를 들어, 서비스에서 사용하고 있는 실제 DB를 거치지 않고 가짜 데이터를 사용하는 것입니다. - -### **2. mocking의 이점** - -그럼 어떤 이점이 있길래 mocking을 하는 걸까요? mocking의 장점을 3가지로 정리해볼 수 있어요. - -1. **외부 서비스나 리소스에 대한 의존성 제거** - - → 외부 API나 데이터베이스와 같은 외부 서비스, 네트워크 지연 등에 영향을 받지 않고 테스트를 수행할 수 있습니다. - -2. **어려운 조건과 상황의 시뮬레이션** - - → 네트워크 오류나 데이터베이스 연결 오류 등과 같은 예외 상황이나 희귀한 상황을 임의로 재현해서 특정 상황에서 애플리케이션이 어떻게 동작하는 지 테스트할 수 있습니다. - -3. **비용 절감** - - → 테스트를 할 때 실제 서버를 이용하면 서버 비용이 부담이 될 수 있어요. 하지만 mocking을 통해 수행하면 실제 리소스를 사용하지 않으므로 테스트 비용을 절감할 수 있습니다. - -### **3. 우리가 모킹을 결정한 이유** - -그렇다면 그로잉 프로젝트에서 서버 데이터를 모킹 하기로 한 이유는 무엇일까요? - -저희는 비용 문제로 인해 서버를 계속 켜놓을 수 없었어요. 그리고 각 기능별로 에러와 로딩 상황에 대한 UI를 수정할 예정이라 이런 상황들을 빠르게 재현하고 적절히 처리하는데 mocking의 도움을 받으면 좋겠다는 생각을 했어요. 마지막으로, 후에 테스트 코드를 작성할 때 mocking 데이터를 재활용할 수 있다는 점도 저희의 결정에 영향을 줬습니다. - -## mocking 도구 비교 분석 - -### 다른 라이브러리 소개 - -- **Nock** - - Node.js 환경에서 HTTP 요청을 mocking하고, 테스팅할 수 있는 강력한 라이브러리에요. 주로 서버 사이드에서 API 호출을 가로채고 대체 응답을 제공하는 데 사용됩니다. -- **Mirage JS** - - 백엔드 없이도 클라이언트 사이드에서 API를 모의할 수 있게 해주는 라이브러리에요. Mirage는 자체 ORM과 라우터를 제공하여 실제 백엔드 서버를 흉내낼 수 있습니다. -- **JSON Server** - - 간단한 REST API를 빠르게 프로토타이핑하고자 할 때 유용한 툴로, JSON 파일을 데이터베이스로 사용하여 실제 서버처럼 작동하는 가짜 API 서버를 생성할 수 있습니다. - -### 라이브러리 비교 - -1. **API support** - - | | MSW | Nock | Mirage | JSON server | - | ------------- | --- | ---- | --------------------------------- | ----------- | - | REST API | ✅ | ✅ | ✅ | ✅ | - | GraphQL API | ✅ | ⛔ | 부분 지원(써드파티 애드온을 통해) | ⛔ | - | WebSocket API | ⛔ | ⛔ | ⛔ | ⛔ | - -2. **Supported environment** - - | | MSW | Nock | Mirage | JSON server | - | ------- | --- | ---- | ---------------------------------------------------------------------------------- | -------------------------------------------- | - | Node.js | ✅ | ✅ | 부분 지원(주로 프런트엔드 위주, Node.js는 Mirage의 서버 사이드 렌더링 지원을 통해) | ✅ | - | Browser | ✅ | ⛔ | ✅ | ⛔ (프록시 서버를 통해 간접적으로 사용 가능) | - -### **MSW만의 특징** - -MSW(Mock Service Worker)를 다른 mocking 도구들과 비교하여 채택한 주요 이유는 다음과 같아요. - -- **실제 네트워크 환경과의 유사성** - - MSW는 서비스 워커를 사용하여 실제 네트워크 요청을 가로채고 모의 응답을 제공해요. 이 접근 방식은 개발자가 실제 백엔드와 통신하는 것처럼 코드를 작성할 수 있게 하며, 나중에 실제 API로의 전환을 매끄럽게 해줘요. 다른 도구들이 라이브러리 레벨에서 모의를 제공하는 것과는 대조적으로, MSW는 네트워크 레벨에서 작동하므로 개발 경험이 더욱 실제 서버와 유시헤요. -- **광범위한 테스트 및 개발 환경 지원** - - MSW는 브라우저 뿐만 아니라 Node.js 환경에서도 작동해요. 또한 Jest와 같은 단위 테스트 프레임워크뿐만 아니라 Cypress, Storybook과 같은 통합 테스트 및 UI 컴포넌트 테스트 환경에서도 사용할 수 있어요. 이는 하나의 모의 설정을 통해 여러 테스팅 환경과 개발 환경에서 일관된 API mocking을 가능하게 도와줍니다. - -## 초기 세팅을 해보아요. - -우선 사용자 데이터를 불러오는 간단한 API를 MSW를 사용해 연동해볼게요. - -### **1. 핸들러 작성하기** - -먼저, API 요청을 가로챌 핸들러를 작성해야 합니다. - -```tsx -// handlers.ts -import { http, HttpResponse, delay } from "msw" - -export const handlers = [ - http.get( - "/user/:userId", - async ({ params }) => { - const { userId } = params - - await delay(1000) - - return HttpResponse.json({ - id: userId, - nickName: "곰곰곰", - birthDay: "2000-01-01", - anniversaryDay: "2023-12-16", - }) - } - ), -] -``` - -> HttpResponse 대신 new Response(..) 를 사용할 수도 있지만, HttpResponse가 1) json(),formData() 같은 유용한 메서드를 지원해주고 2) set-cookie를 설정할 수도 있어서 HttpResponse를 사용했어요. - -### **2. 브라우저 환경과 통합하기** - -MSW를 웹 애플리케이션과 통합하기 위해서는 몇 가지 단계를 더 거쳐야 해요. - -1. **Worker script 설치** - - 아래 명령어를 실행합니다. - - ```powershell - npx msw init ./public --save - ``` - -2. **Worker 설정** - - MSW Worker를 설정하고 핸들러를 등록합니다. - - ```tsx - // browser.ts - import { setupWorker } from "msw/browser" - import { handlers } from "./handlers" - - export const worker = setupWorker(...handlers) - ``` - -3. **Worker 등록 및 실행** - - 개발 환경에서만 MSW를 활성화하기 위해 조건을 설정하고, 애플리케이션 실행 전에 Worker를 등록합니다. - - ```tsx - // msw.ts - const enableMocking = async () => { - if (process.env.NODE_ENV !== "development") { - return - } - - const { worker } = await import("./mocks/browser") - - return worker.start() - } - - // index.tsx - const root = ReactDOM.createRoot(document.getElementById("root")) - enableMocking().then(() => { - root - .render - // 애플리케이션 컴포넌트 - () - }) - ``` - -### **3. 콘솔에서 확인하기** - -애플리케이션을 실행한 후, 개발자 도구의 콘솔에서 MSW가 정상적으로 작동하는지 확인합니다. - -![](console-1-image.png) - -잘 작동하네요! 😮😮 - -### **4. Mocking하지 않은 API 처리하기** - -그런데 콘솔에는 많은 경고가 존재하고 있었어요. +## 툴바를 만들자! -![](console-2-image.png) +저희 그로잉팀은 런타임에서 여러 기능들의 성공과 실패의 조합에 따라 테스트를 할 수 있는 방식이 필요했고, 이를 MSW Toolbar를 만들어 해결했어요. -이를 알아본 결과, MSW는 정의되지 않은 API 요청을 모두 캐치해 경고를 출력한다고 해요. +이번 글에서는 MSW Toolbar 제작 과정을 소개할게요.
- +
-따라서 onUnhandledRequest 옵션을 bypass로 설정하여 정의되지 않은 API 요청은 무시하도록 설정했어요. - -```tsx -worker.start({ onUnhandledRequest: "bypass" }) -``` - -## 폴더 구조를 잡아봅시다. - -간단하게 테스트해 본 결과, msw도입을 위해 필요한 파일들은 크게 3가지 종류로 나눌 수 있었어요. - -1. API 핸들러 -2. Mock 데이터 -3. 브라우저 환경에서 실행할 수 있게 도와주는 Worker함수 - -따라서 각 파일들을 어떤 디렉터리에 위치시킬지 생각해보았어요. - -**방법 1: API 경로 기반 구조** - -첫번째로 고려한 방식은 API 주소를 기준으로 폴더를 구성하는 방식이에요. - -예를 들어 `http://localhost/test/verification`이라는 API를 Mocking한다고 했을 때, 파일구조는 아래와 같이 작성됩니다. - -``` -src -├── __mocks__/ -│ ├── localhost/ -│ │ └── test/ -│ │ └── verification -│ ├── handlers.ts -│ └── browser.ts -``` - -**장점은 다음과 같아요.** - -- **직관적인 경로:** API 경로를 기반으로 하는 구조는 파일 시스템에서 해당 API를 쉽게 찾을 수 있게 해줍니다. 이는 특히 큰 프로젝트에서 API를 빠르게 찾아 수정해야 할 때 유용해요. -- **변경 관리 용이:** 특정 API에 대한 변경 사항이 있을 때, 관련 파일을 찾아 수정하기가 간편합니다. - -**단점은 다음과 같아요.** - -- **중복된 구조:** 여러 API가 비슷한 데이터 구조를 공유할 경우, 코드와 mock 데이터의 중복이 발생할 수 있습니다. -- **스케일링 문제:** 프로젝트 규모가 커지면 폴더 구조가 깊어지고 복잡해질 수 있습니다. - -**방법 2: Mock 데이터 중심 구조** - -두번째로 고려해 본 방식은 Mock 데이터만 별도로 관리하고 핸들러는 한 곳에 모으는 방식입니다. - -``` -src -├── server/ -│ ├── __mocks__/ -│ │ └── userList.ts/ -│ ├── handlers.ts -│ └── browser.ts -``` - -**장점은 다음과 같아요.** - -- **데이터 중심:** 공통된 데이터 구조를 사용하는 API들이 많은 경우, mock 데이터를 중앙에서 관리함으로써 중복을 줄일 수 있습니다. -- **유연성:** 데이터 변경이 필요할 때 한 곳에서 관리하기 때문에, 데이터의 일관성을 유지하기가 용이합니다. - -**단점은 다음과 같아요.** - -- **핸들러 관리:** 모든 핸들러를 한 곳에 모으게 되면, 파일이 방대해지고 관리가 어려워질 수 있습니다. -- **데이터와 핸들러의 분리:** 데이터와 핸들러가 분리되어 있어, 관련 핸들러와 데이터 사이의 연결을 파악하기 어려울 수 있습니다. - -**방법 3: 도메인별 구조** - -세번째로 고려한 방식은 도메인별로 핸들러와 데이터를 모아두는 방식이에요. - -``` -src -├── mocks/ -│ ├── domain/ -│ │ └── album/ -│ │ ├── data.ts -│ │ └── handlers.ts -│ ├── handlers.ts -│ └── browser.ts -``` - -**장점은 다음과 같아요.** - -- **도메인 중심:** 서비스의 기능별로 구분되어 있어, 관련 기능을 개발할 때 모든 관련 파일을 쉽게 찾을 수 있습니다. -- **유지보수 용이:** 각 도메인별로 핸들러와 데이터를 분리함으로써, 유지보수가 용이합니다. 특정 기능에 문제가 생겼을 때, 해당 도메인의 폴더만 확인하면 됩니다. -- **확장성:** 새로운 기능이나 도메인이 추가될 때, 새로운 폴더를 만들어 그 안에 모든 관련 파일을 배치함으로써 확장성이 높습니다. - -**단점은 다음과 같아요.** - -- **공통 데이터 관리:** 여러 도메인에서 공통적으로 사용되는 데이터가 있을 경우, 이를 어떻게 관리할지 고민이 필요합니다. -- **API 간 데이터 공유의 어려움:** 특정 도메인에서만 사용되는 데이터가 아닌, 전역적으로 사용되는 데이터의 경우, 어느 도메인에 속해야 할지 결정하기 어려울 수 있습니다. - -**그로잉은요?** - -그로잉 서비스는 각 기능별로 구분된 도메인이 명확하기 때문에, 3번 방식이 가장 적합하다고 판단했어요. - -그리고 기존에 외부 라이브러리는 libs폴더에 정의해두고 사용했지만, msw 관련 파일은 mocks폴더에 전부 모아두기로 했어요. 빌드시 필요없는 코드와 파일들을 한 곳에 몰아넣기 위함이에요. - -또한 3번 방식처럼 도메인별 핸들러 함수를 한 파일에 전부 정의해두는 것보다는 분리한 후 index 파일에서 취합해서 내보내기로 했어요. API별 데이터들과 params, 200일때, 400일때 케이스들을 한 파일에 모두 작성하면 코드의 길이가 너무 길어져서 가독성을 해칠 것이라고 판단했기 때문이에요. - -따라서 아래와 같이 폴더 구조를 정할 수 있었어요. - -``` -src -├── mocks/ -│ ├── user/ -│ │ ├── index.ts -│ │ ├── getUserHandler.ts -│ │ └── anotherHandler.ts -│ ├── handlers.ts // 도메인별 핸들러 함수를 모두 취합하는 곳이에요. -│ ├── browser.ts // worker를 정의해 둔 파일이에요. -│ └── msw.ts // enableMocking() 함수를 정의해 둔 파일이에요. -``` - -아래는 예시입니다. - -```tsx -// mocks/user/getUserHandler.ts -type Params = { - userId: string -} - -const data: UserDto = { - id: "1", - nickName: "곰곰곰", - birthDay: "2000-01-01", - anniversaryDay: "2023-12-16", -} - -export const getUserHandler = () => { - http.get( - "/user/:userId", - async ({ params }) => { - const { userId } = params - - await delay(1000) - - return HttpResponse.json(data) - } - ) -} - -// mocks/user/index.ts -import { getUserHandler } from "./getUserHandler" -import { anotherHandler } from "./anotherHandler" - -export const UserHandlers = [getUserHandler, anotherHandler] -``` - -그러나 이 방식은 일부 도메인에는 적용할 수 없었어요. - -GET, POST, DELETE 하는 API 가 같은 데이터를 바라봐야 했기 때문이죠. 만약 기존 데이터를 DELETE 요청으로 지운 후에 GET 요청을 했을 때, 지운 데이터가 남아 있다면 테스트할 때 불편하다고 판단했어요. - -최최최최종.jpg - -따라서 데이터를 공유해야하는 API 들은 한 파일 안에 작성해두는 것으로 변경했어요. - -``` -src -├── mocks/ -│ ├── user/ -│ │ ├── getUserHandler.ts // 데이터를 공유할 필요없는 API -│ │ ├── ... -│ │ └── index.ts -│ ├── chat/ -│ │ ├── data/ -│ │ │ ├── chatData.ts // 공유할 데이터 -│ │ │ └── image.png -│ │ ├── chatHandler.ts // 공유할 데이터들을 사용하는 API들 -│ │ ├── chatQuestionHandler.ts -│ │ └── index.ts -│ ├── handlers.ts -│ └── browser.ts -``` - -## 에러 응답 목업은 어떻게 하지? 🤔 - -애플리케이션은 완벽할 수 없습니다. 네트워크 에러나 우리가 미처 처리하지 못한 예외 등으로 인해 생각한 대로 동작하지 않기도 해요. 그렇기 때문에 성공적인 응답만 mocking하는 것으로는 부족합니다. 이번 절에서는 에러 응답에 대한 모킹 방법에 대해 알아볼게요. - -### MSW **공식 문서 살펴보기** - -MSW의 공식 문서에서 다음과 같은 에러 응답 목업 방식에 대해 찾을 수 있었어요. - -- status code 설정하기 - - ```jsx - export const handlers = [ - http.delete("/posts/:id", ({ params }) => { - const { id } = params - const deletedPost = allPosts.get(id) - - if (!deletedPost) { - return new HttpResponse(null, { status: 404 }) - } - - allPosts.delete(id) - - return HttpResponse.json(deletedPost) - }), - ] - ``` - - → 조건에 따라 에러를 나타내는 status를 함께 반환하는 방식이에요. - -- Network Error - - ```jsx - import { http, HttpResponse } from "msw" - - export const handlers = [ - http.get("/resource", () => { - return HttpResponse.error() - }), - ] - ``` - - → 네트워크 에러의 경우 위와 같이 HttpResponse.error()를 반환해요. - -- dynamic mock scenarios - - ```jsx - import { http, HttpResponse } from "msw" - - export const scenarios = { - success: [ - http.get("/user", () => { - return HttpResponse.json({ name: "minju" }) - }), - ], - error: [ - http.get("/user", () => { - return new HttpResponse(null, { status: 500 }) - }), - ], - } - ``` - - → 응답에 성공하는 경우와, 에러인 경우를 시나리오로 작성할 수 있어요. url 뒤에 `?scenario=error` 을 붙이면 오류 응답을 반환하는 경우에 어떻게 작동하는지 런타임에서 확인할 수 있어요. - -### 그로잉에서 선택한 에러 목업 - -저희는 런타임에서 여러 기능들의 성공과 실패의 조합에 따라 테스트를 할 수 있는 방식이 필요했어요. msw에서 제공해주는 dynamic mock scenarios를 사용해 이를 구현하려면 url이 바뀌여야 하고, 한 페이지에 있는 여러 기능에 대해 각 시나리오를 식별하도록 코드를 추가로 작성해야 합니다. 그래서 저희는 status code를 런타임에 다르게 설정하는 방식을 사용하기로 했고, 여러 조합을 편하게 테스트할 수 있게 도와주는 툴바를 제작하기로 했어요. - -## 툴바를 만들자! - ### 정의된 핸들러 리스트 받아오기 툴바 구현을 위해 먼저 정의해 둔 핸들러 리스트 정보들을 가져올 수 있는 지가 가장 중요했어요. 따라서 해당 정보를 받아올 수 있는지 확인하는 작업을 먼저 진행했어요. @@ -482,6 +81,7 @@ export const getUserSuccessHandler = () => { }); } +// 실패 응답 핸들러 export const getUserErrorHandler = () => { http.get( '/user/:userId', @@ -651,7 +251,7 @@ const handleClick = (path: string, method: string) => { 모든 API 핸들러에서 반복적으로 나타나는 패턴들은 무엇이 있을까요? -다음 코드가 반복해서 작성되어야 했어요. 이를 추상화하여 코드 중복을 줄여볼게요. +코드를 분석해 본 결과 다음 코드가 반복해서 작성되어야 했어요. 이를 추상화하여 코드 중복을 줄여볼게요. ```tsx const handler = handlerInfoManager.getHandlerInfo("/couples/:coupleId", "GET") @@ -847,6 +447,8 @@ export const deleteOurChatHandler = createApiHandler< 이제 툴바 컴포넌트를 제작해 볼게요. + + ```tsx //MSWToolbar.tsx import ... @@ -937,10 +539,6 @@ function MSWToolbar() { export default MSWToolbar; ``` -
- -
- 툴바 컴포넌트의 전체 코드와 툴바의 옵션 선택 부분입니다. 코드의 동작 과정을 자세히 살펴볼게요. - `handlerInfoManager.getHandlerInfos()` 를 사용해 handler의 path와 method를 받아오고 있어요. @@ -949,9 +547,13 @@ export default MSWToolbar; - 적용하기 버튼을 누르면, `clickApplyBtnHandler` 가 실행되어 stagedValue에 저장된 옵션들(delay와 status code)이 실제 핸들러에 적용됩니다. 현재 그로잉 프로젝트에서는 react-query를 사용하고 있어서 옵션들을 적용한 후 바로 데이터를 refetch하도록 만들기 위해 마지막으로 `queryClient.invalidateQueries()` 를 실행해주고 있어요. - real은 현실적인 응답 시간 정도를 지연해주는 옵션입니다. infinite는 응답을 무한하게 delay 시켜주는 옵션입니다. 이는 로딩 상태가 잘 표시되는 지 등을 확인할 때 유용하게 사용할 수 있어요. -코드에서 한 가지 생각해볼 점이 있었어요. +#### 코드에서 한 가지 생각해볼 점이 있었어요. + +`const stagedValue = useRef<{[key: string]: SetHandlerParams;}>({});` 부분에서 왜 useRef 훅을 사용했을까요? -`const stagedValue = useRef<{[key: string]: SetHandlerParams;}>({});` 부분에서 왜 useRef 훅을 사용했을까요? stagedValue를 useRef없이 사용하면, 검색을 할 때마다 MSWToolbar 컴포넌트가 재실행 되어 stagedValue가 빈 객체로 초기화됩니다. 이를 방지하고자 useState 훅을 사용하면, stagedValue가 변경 될 때마다 리렌더링이 발생해요. 그래서 컴포넌트가 재실행 되더라도 stagedValue값을 유지하면서, stagedValue가 변경되더라도 리렌더링이 일어나지 않게 하기 위해 useRef hook을 사용했어요. +stagedValue를 useRef없이 사용하면, 검색을 할 때마다 MSWToolbar 컴포넌트가 재실행 되어 stagedValue가 빈 객체로 초기화됩니다. 이를 방지하고자 useState 훅을 사용하면, stagedValue가 변경 될 때마다 리렌더링이 발생해요. + +그래서 컴포넌트가 재실행 되더라도 stagedValue값을 유지하면서, stagedValue가 변경되더라도 리렌더링이 일어나지 않게 하기 위해 useRef hook을 사용했어요. ### 툴바는 리액트 돔 트리에서 어디에 위치해야 할까? @@ -986,25 +588,23 @@ enableMocking().then(() => { }) ``` -다음은 MSWToolbar 컴포넌트의 위치를 선택할 때 고려한 내용들이에요. +**다음은 MSWToolbar 컴포넌트의 위치를 선택할 때 고려한 내용들이에요.** 1. MSWToolbar는 dev모드에서만 뜨도록 해야 합니다. 2. MSWToolbar는 런타임에 영향을 주지 않아야 합니다. 3. MSWToolbar에서 styled-component의 theme을 이용하고 있어 ThemeProvider 안에 들어가야 합니다. 4. react query로 관리되고 있는 서버 데이터의 invalidate해줄 수 있어야 하므로 QueryClientProvider 안에 들어가야 합니다. -1번을 위해 NODE_ENV에 따라 Toolbar를 표시해 주었고, 2번,3번을 고려해 AsyncBoundary밖이면서 ThemeProvider와 QueryClientProvider의 안에 위치 시켰어요. MSWToolbar를 이용해 의도적으로 error 상황을 테스트할 때, 그로잉 프로젝트에서 AsyncBoudary와 함께 사용하고 있는 FullScreenError가 뜰 수 있어요. 이때, MSWToolbar는 런타임에서 발생하는 에러 범위 밖에서 상태코드를 200으로 바꿔줄 수 있어야 하기 때문에 AsyncBoundary밖에 있어야 해요. +1번을 위해 NODE_ENV에 따라 Toolbar를 표시해 주었고 2, 3번을 고려해 AsyncBoundary밖이면서 ThemeProvider와 QueryClientProvider의 안에 위치 시켰어요. -## 최종 완성 +MSWToolbar를 이용해 의도적으로 error 상황을 테스트할 때, 그로잉 프로젝트에서 AsyncBoundary와 함께 사용하고 있는 FullScreenError가 뜰 수 있어요. 이때, MSWToolbar는 런타임에서 발생하는 에러 범위 밖에서 상태코드를 200으로 바꿔줄 수 있어야 하기 때문에 AsyncBoundary밖에 위치시켜두었어요. -### 시연영상 +## 결론 -
- -
+이 과정을 통해 MSW와 MSW를 활용한 Toolbar 도입에 성공할 수 있었어요. -## 결론 +MSW를 통한 API 모킹은 API 경로를 기반으로 이루어지므로, 각 요청을 명확하게 식별하고 관리할 수 있었어요. -MSW와 MSW를 활용한 툴바의 도입을 성공했습니다. 도입 전에는 로딩 상태 확인을 위해 크롬 devtools를 활용했는데 툴바를 활용해 런타임에서 쉽게 delay를 조정할 수 있게 되었습니다. 그리고 에러가 발생하는 상황을 쉽게 재현하고, 다시 응답에 성공하도록 되돌리는 것도 간편해졌습니다. +MSW Toolbar는 네트워크 요청의 시뮬레이션 과정을 간소화해주는데 큰 도움을 주었어요. 기존에는 네트워크 상태를 모니터링하고 조정하기 위해 크롬의 개발자 도구에 의존했지만, 이제는 Toolbar를 통해 직접적으로 응답 시간을 조절하거나 특정 API 요청에서 에러 상태를 유발하는 등의 작업을 쉽게 처리할 수 있게 되었어요. 이에 따라 다양한 네트워크 환경과 에러 상황을 빠르게 시뮬레이션하고 대응할 수 있을 것으로 기대하고 있어요. -MSW를 사용한 모킹은 api path를 기준으로 이루어지기 때문에 매우 직관적이고 코드 작성도 쉬웠던 것 같습니다. +이를 통해 개발 과정에서의 시행착오를 줄이고, 개발 효율성을 높여가며, 최종적으로 사용자에게 더 나은 서비스를 제공하는 그로잉 팀이 되어볼게요. 감사합니다 :) diff --git a/contents/posts/msw-toolbar/console-1-image.png b/contents/posts/msw/console-1-image.png similarity index 100% rename from contents/posts/msw-toolbar/console-1-image.png rename to contents/posts/msw/console-1-image.png diff --git a/contents/posts/msw-toolbar/console-2-image.png b/contents/posts/msw/console-2-image.png similarity index 100% rename from contents/posts/msw-toolbar/console-2-image.png rename to contents/posts/msw/console-2-image.png diff --git a/contents/posts/msw-toolbar/console-3-image.png b/contents/posts/msw/console-3-image.png similarity index 100% rename from contents/posts/msw-toolbar/console-3-image.png rename to contents/posts/msw/console-3-image.png diff --git a/contents/posts/msw/index.md b/contents/posts/msw/index.md new file mode 100644 index 0000000..567eb5d --- /dev/null +++ b/contents/posts/msw/index.md @@ -0,0 +1,425 @@ +--- +title: "MSW로 mocking을 해보자 " +description: "그로잉 팀의 MSW 도입 과정을 소개할게요." +date: 2024-02-24 +update: 2024-02-24 +tags: + - growing + - msw + - mocking +series: "MSW Toolbar로 다양한 시나리오 테스트하기" +--- + +## mocking에 대해 알아보자 + +### **1. mocking 이란?** + +**모킹**은 특정 객체의 실제 구현 대신 가짜 객체를 사용해서 그 객체의 행동을 모방하는 프로세스입니다. 이러한 가짜 객체를 'mock 객체'라고 해요. Mock 객체는 우리의 서비스가 의존하고 있는 외부 시스템이나 복잡한 로직 없이도 동작할 수 있도록 해줍니다. 예를 들어, 서비스에서 사용하고 있는 실제 DB를 거치지 않고 가짜 데이터를 사용하는 것입니다. + +### **2. mocking의 이점** + +그럼 어떤 이점이 있길래 mocking을 하는 걸까요? mocking의 장점을 3가지로 정리해볼 수 있어요. + +1. **외부 서비스나 리소스에 대한 의존성 제거** + + → 외부 API나 데이터베이스와 같은 외부 서비스, 네트워크 지연 등에 영향을 받지 않고 테스트를 수행할 수 있습니다. + +2. **어려운 조건과 상황의 시뮬레이션** + + → 네트워크 오류나 데이터베이스 연결 오류 등과 같은 예외 상황이나 희귀한 상황을 임의로 재현해서 특정 상황에서 애플리케이션이 어떻게 동작하는 지 테스트할 수 있습니다. + +3. **비용 절감** + + → 테스트를 할 때 실제 서버를 이용하면 서버 비용이 부담이 될 수 있어요. 하지만 mocking을 통해 수행하면 실제 리소스를 사용하지 않으므로 테스트 비용을 절감할 수 있습니다. + +### **3. 우리가 모킹을 결정한 이유** + +그렇다면 그로잉 프로젝트에서 서버 데이터를 모킹 하기로 한 이유는 무엇일까요? + +저희는 비용 문제로 인해 서버를 계속 켜놓을 수 없었어요. 그리고 각 기능별로 에러와 로딩 상황에 대한 UI를 수정할 예정이라 이런 상황들을 빠르게 재현하고 적절히 처리하는데 mocking의 도움을 받으면 좋겠다는 생각을 했어요. 마지막으로, 후에 테스트 코드를 작성할 때 mocking 데이터를 재활용할 수 있다는 점도 저희의 결정에 영향을 줬습니다. + +## mocking 도구 비교 분석 + +### 다른 라이브러리 소개 + +- **Nock** + - Node.js 환경에서 HTTP 요청을 mocking하고, 테스팅할 수 있는 강력한 라이브러리에요. 주로 서버 사이드에서 API 호출을 가로채고 대체 응답을 제공하는 데 사용됩니다. +- **Mirage JS** + - 백엔드 없이도 클라이언트 사이드에서 API를 모의할 수 있게 해주는 라이브러리에요. Mirage는 자체 ORM과 라우터를 제공하여 실제 백엔드 서버를 흉내낼 수 있습니다. +- **JSON Server** + - 간단한 REST API를 빠르게 프로토타이핑하고자 할 때 유용한 툴로, JSON 파일을 데이터베이스로 사용하여 실제 서버처럼 작동하는 가짜 API 서버를 생성할 수 있습니다. + +### 라이브러리 비교 + +1. **API support** + + | | MSW | Nock | Mirage | JSON server | + | ------------- | --- | ---- | --------------------------------- | ----------- | + | REST API | ✅ | ✅ | ✅ | ✅ | + | GraphQL API | ✅ | ⛔ | 부분 지원(써드파티 애드온을 통해) | ⛔ | + | WebSocket API | ⛔ | ⛔ | ⛔ | ⛔ | + +2. **Supported environment** + + | | MSW | Nock | Mirage | JSON server | + | ------- | --- | ---- | ---------------------------------------------------------------------------------- | -------------------------------------------- | + | Node.js | ✅ | ✅ | 부분 지원(주로 프런트엔드 위주, Node.js는 Mirage의 서버 사이드 렌더링 지원을 통해) | ✅ | + | Browser | ✅ | ⛔ | ✅ | ⛔ (프록시 서버를 통해 간접적으로 사용 가능) | + +### **MSW만의 특징** + +MSW(Mock Service Worker)를 다른 mocking 도구들과 비교하여 채택한 주요 이유는 다음과 같아요. + +- **실제 네트워크 환경과의 유사성** + - MSW는 서비스 워커를 사용하여 실제 네트워크 요청을 가로채고 모의 응답을 제공해요. 이 접근 방식은 개발자가 실제 백엔드와 통신하는 것처럼 코드를 작성할 수 있게 하며, 나중에 실제 API로의 전환을 매끄럽게 해줘요. 다른 도구들이 라이브러리 레벨에서 모의를 제공하는 것과는 대조적으로, MSW는 네트워크 레벨에서 작동하므로 개발 경험이 더욱 실제 서버와 유시헤요. +- **광범위한 테스트 및 개발 환경 지원** + - MSW는 브라우저 뿐만 아니라 Node.js 환경에서도 작동해요. 또한 Jest와 같은 단위 테스트 프레임워크뿐만 아니라 Cypress, Storybook과 같은 통합 테스트 및 UI 컴포넌트 테스트 환경에서도 사용할 수 있어요. 이는 하나의 모의 설정을 통해 여러 테스팅 환경과 개발 환경에서 일관된 API mocking을 가능하게 도와줍니다. + +## 초기 세팅을 해보아요. + +우선 사용자 데이터를 불러오는 간단한 API를 MSW를 사용해 연동해볼게요. + +### **1. 핸들러 작성하기** + +먼저, API 요청을 가로챌 핸들러를 작성해야 합니다. + +```tsx +// handlers.ts +import { http, HttpResponse, delay } from "msw" + +export const handlers = [ + http.get( + "/user/:userId", + async ({ params }) => { + const { userId } = params + + await delay(1000) + + return HttpResponse.json({ + id: userId, + nickName: "곰곰곰", + birthDay: "2000-01-01", + anniversaryDay: "2023-12-16", + }) + } + ), +] +``` + +> HttpResponse 대신 new Response(..) 를 사용할 수도 있지만, HttpResponse가 1) json(),formData() 같은 유용한 메서드를 지원해주고 2) set-cookie를 설정할 수도 있어서 HttpResponse를 사용했어요. + +### **2. 브라우저 환경과 통합하기** + +MSW를 웹 애플리케이션과 통합하기 위해서는 몇 가지 단계를 더 거쳐야 해요. + +1. **Worker script 설치** + + 아래 명령어를 실행합니다. + + ```powershell + npx msw init ./public --save + ``` + +2. **Worker 설정** + + MSW Worker를 설정하고 핸들러를 등록합니다. + + ```tsx + // browser.ts + import { setupWorker } from "msw/browser" + import { handlers } from "./handlers" + + export const worker = setupWorker(...handlers) + ``` + +3. **Worker 등록 및 실행** + + 개발 환경에서만 MSW를 활성화하기 위해 조건을 설정하고, 애플리케이션 실행 전에 Worker를 등록합니다. + + ```tsx + // msw.ts + const enableMocking = async () => { + if (process.env.NODE_ENV !== "development") { + return + } + + const { worker } = await import("./mocks/browser") + + return worker.start() + } + + // index.tsx + const root = ReactDOM.createRoot(document.getElementById("root")) + enableMocking().then(() => { + root + .render + // 애플리케이션 컴포넌트 + () + }) + ``` + +### **3. 콘솔에서 확인하기** + +애플리케이션을 실행한 후, 개발자 도구의 콘솔에서 MSW가 정상적으로 작동하는지 확인합니다. + +![](console-1-image.png) + +잘 작동하네요! 😮😮 + +### **4. Mocking하지 않은 API 처리하기** + +그런데 콘솔에는 많은 경고가 존재하고 있었어요. + +![](console-2-image.png) + +이를 알아본 결과, MSW는 정의되지 않은 API 요청을 모두 캐치해 경고를 출력한다고 해요. + +
+ +
+ +따라서 onUnhandledRequest 옵션을 bypass로 설정하여 정의되지 않은 API 요청은 무시하도록 설정했어요. + +```tsx +worker.start({ onUnhandledRequest: "bypass" }) +``` + +## 폴더 구조를 잡아봅시다. + +간단하게 테스트해 본 결과, msw도입을 위해 필요한 파일들은 크게 3가지 종류로 나눌 수 있었어요. + +1. API 핸들러 +2. Mock 데이터 +3. 브라우저 환경에서 실행할 수 있게 도와주는 Worker함수 + +따라서 각 파일들을 어떤 디렉터리에 위치시킬지 생각해보았어요. + +### 방법 1: API 경로 기반 구조 + +첫번째로 고려한 방식은 API 주소를 기준으로 폴더를 구성하는 방식이에요. + +예를 들어 `http://localhost/test/verification`이라는 API를 Mocking한다고 했을 때, 파일구조는 아래와 같이 작성됩니다. + +``` +src +├── __mocks__/ +│ ├── localhost/ +│ │ └── test/ +│ │ └── verification +│ ├── handlers.ts +│ └── browser.ts +``` + +**장점은 다음과 같아요.** + +- **직관적인 경로:** API 경로를 기반으로 하는 구조는 파일 시스템에서 해당 API를 쉽게 찾을 수 있게 해줍니다. 이는 특히 큰 프로젝트에서 API를 빠르게 찾아 수정해야 할 때 유용해요. +- **변경 관리 용이:** 특정 API에 대한 변경 사항이 있을 때, 관련 파일을 찾아 수정하기가 간편합니다. + +**단점은 다음과 같아요.** + +- **중복된 구조:** 여러 API가 비슷한 데이터 구조를 공유할 경우, 코드와 mock 데이터의 중복이 발생할 수 있습니다. +- **스케일링 문제:** 프로젝트 규모가 커지면 폴더 구조가 깊어지고 복잡해질 수 있습니다. + +### 방법 2: Mock 데이터 중심 구조 + +두번째로 고려해 본 방식은 Mock 데이터만 별도로 관리하고 핸들러는 한 곳에 모으는 방식입니다. + +``` +src +├── server/ +│ ├── __mocks__/ +│ │ └── userList.ts/ +│ ├── handlers.ts +│ └── browser.ts +``` + +**장점은 다음과 같아요.** + +- **데이터 중심:** 공통된 데이터 구조를 사용하는 API들이 많은 경우, mock 데이터를 중앙에서 관리함으로써 중복을 줄일 수 있습니다. +- **유연성:** 데이터 변경이 필요할 때 한 곳에서 관리하기 때문에, 데이터의 일관성을 유지하기가 용이합니다. + +**단점은 다음과 같아요.** + +- **핸들러 관리:** 모든 핸들러를 한 곳에 모으게 되면, 파일이 방대해지고 관리가 어려워질 수 있습니다. +- **데이터와 핸들러의 분리:** 데이터와 핸들러가 분리되어 있어, 관련 핸들러와 데이터 사이의 연결을 파악하기 어려울 수 있습니다. + +### 방법 3: 도메인별 구조 + +세번째로 고려한 방식은 도메인별로 핸들러와 데이터를 모아두는 방식이에요. + +``` +src +├── mocks/ +│ ├── domain/ +│ │ └── album/ +│ │ ├── data.ts +│ │ └── handlers.ts +│ ├── handlers.ts +│ └── browser.ts +``` + +**장점은 다음과 같아요.** + +- **도메인 중심:** 서비스의 기능별로 구분되어 있어, 관련 기능을 개발할 때 모든 관련 파일을 쉽게 찾을 수 있습니다. +- **유지보수 용이:** 각 도메인별로 핸들러와 데이터를 분리함으로써, 유지보수가 용이합니다. 특정 기능에 문제가 생겼을 때, 해당 도메인의 폴더만 확인하면 됩니다. +- **확장성:** 새로운 기능이나 도메인이 추가될 때, 새로운 폴더를 만들어 그 안에 모든 관련 파일을 배치함으로써 확장성이 높습니다. + +**단점은 다음과 같아요.** + +- **공통 데이터 관리:** 여러 도메인에서 공통적으로 사용되는 데이터가 있을 경우, 이를 어떻게 관리할지 고민이 필요합니다. +- **API 간 데이터 공유의 어려움:** 특정 도메인에서만 사용되는 데이터가 아닌, 전역적으로 사용되는 데이터의 경우, 어느 도메인에 속해야 할지 결정하기 어려울 수 있습니다. + +### 그로잉은요? + +그로잉 서비스는 각 기능별로 구분된 도메인이 명확하기 때문에, 3번 방식이 가장 적합하다고 판단했어요. + +그리고 기존에 외부 라이브러리는 libs폴더에 정의해두고 사용했지만, msw 관련 파일은 mocks폴더에 전부 모아두기로 했어요. 빌드시 필요없는 코드와 파일들을 한 곳에 몰아넣기 위함이에요. + +또한 3번 방식처럼 도메인별 핸들러 함수를 한 파일에 전부 정의해두는 것보다는 분리한 후 index 파일에서 취합해서 내보내기로 했어요. API별 데이터들과 params, 200일때, 400일때 케이스들을 한 파일에 모두 작성하면 코드의 길이가 너무 길어져서 가독성을 해칠 것이라고 판단했기 때문이에요. + +따라서 아래와 같이 폴더 구조를 정할 수 있었어요. + +``` +src +├── mocks/ +│ ├── user/ +│ │ ├── index.ts +│ │ ├── getUserHandler.ts +│ │ └── anotherHandler.ts +│ ├── handlers.ts // 도메인별 핸들러 함수를 모두 취합하는 곳이에요. +│ ├── browser.ts // worker를 정의해 둔 파일이에요. +│ └── msw.ts // enableMocking() 함수를 정의해 둔 파일이에요. +``` + +아래는 예시입니다. + +```tsx +// mocks/user/getUserHandler.ts +type Params = { + userId: string +} + +const data: UserDto = { + id: "1", + nickName: "곰곰곰", + birthDay: "2000-01-01", + anniversaryDay: "2023-12-16", +} + +export const getUserHandler = () => { + http.get( + "/user/:userId", + async ({ params }) => { + const { userId } = params + + await delay(1000) + + return HttpResponse.json(data) + } + ) +} + +// mocks/user/index.ts +import { getUserHandler } from "./getUserHandler" +import { anotherHandler } from "./anotherHandler" + +export const UserHandlers = [getUserHandler, anotherHandler] +``` + +그러나 이 방식은 일부 도메인에는 적용할 수 없었어요. + +GET, POST, DELETE 하는 API 가 같은 데이터를 바라봐야 했기 때문이죠. 만약 기존 데이터를 DELETE 요청으로 지운 후에 GET 요청을 했을 때, 지운 데이터가 남아 있다면 테스트할 때 불편하다고 판단했어요. + +### 최최최최종.jpg + +따라서 데이터를 공유해야하는 API 들은 한 파일 안에 작성해두는 것으로 변경했어요. + +``` +src +├── mocks/ +│ ├── user/ +│ │ ├── getUserHandler.ts // 데이터를 공유할 필요없는 API +│ │ ├── ... +│ │ └── index.ts +│ ├── chat/ +│ │ ├── data/ +│ │ │ ├── chatData.ts // 공유할 데이터 +│ │ │ └── image.png +│ │ ├── chatHandler.ts // 공유할 데이터들을 사용하는 API들 +│ │ ├── chatQuestionHandler.ts +│ │ └── index.ts +│ ├── handlers.ts +│ └── browser.ts +``` + +## 에러 응답 목업은 어떻게 하지? 🤔 + +애플리케이션은 완벽할 수 없습니다. 네트워크 에러나 우리가 미처 처리하지 못한 예외 등으로 인해 생각한 대로 동작하지 않기도 해요. 그렇기 때문에 성공적인 응답만 mocking하는 것으로는 부족합니다. 이번 절에서는 에러 응답에 대한 모킹 방법에 대해 알아볼게요. + +### MSW 공식 문서 살펴보기 + +MSW의 공식 문서에서 다음과 같은 에러 응답 목업 방식에 대해 찾을 수 있었어요. + +- status code 설정하기 + + ```jsx + export const handlers = [ + http.delete("/posts/:id", ({ params }) => { + const { id } = params + const deletedPost = allPosts.get(id) + + if (!deletedPost) { + return new HttpResponse(null, { status: 404 }) + } + + allPosts.delete(id) + + return HttpResponse.json(deletedPost) + }), + ] + ``` + + → 조건에 따라 에러를 나타내는 status를 함께 반환하는 방식이에요. + +- Network Error + + ```jsx + import { http, HttpResponse } from "msw" + + export const handlers = [ + http.get("/resource", () => { + return HttpResponse.error() + }), + ] + ``` + + → 네트워크 에러의 경우 위와 같이 HttpResponse.error()를 반환해요. + +- dynamic mock scenarios + + ```jsx + import { http, HttpResponse } from "msw" + + export const scenarios = { + success: [ + http.get("/user", () => { + return HttpResponse.json({ name: "minju" }) + }), + ], + error: [ + http.get("/user", () => { + return new HttpResponse(null, { status: 500 }) + }), + ], + } + ``` + + → 응답에 성공하는 경우와, 에러인 경우를 시나리오로 작성할 수 있어요. url 뒤에 `?scenario=error` 을 붙이면 오류 응답을 반환하는 경우에 어떻게 작동하는지 런타임에서 확인할 수 있어요. + +### 그로잉에서 선택한 에러 목업 + +저희는 런타임에서 여러 기능들의 성공과 실패의 조합에 따라 테스트를 할 수 있는 방식이 필요했어요. msw에서 제공해주는 dynamic mock scenarios를 사용해 이를 구현하려면 url이 바뀌여야 하고, 한 페이지에 있는 여러 기능에 대해 각 시나리오를 식별하도록 코드를 추가로 작성해야 합니다. 그래서 저희는 status code를 런타임에 다르게 설정하는 방식을 사용하기로 했고, 여러 조합을 편하게 테스트할 수 있게 도와주는 툴바를 제작하기로 했어요. + +
+ +> 🤔 어떻게 MSW Toolbar를 만들었는지 궁금하다면? ➡️ [다음글에서 계속](https://teamgrowing.github.io/team-blog/msw-toolbar) diff --git a/gatsby-config.js b/gatsby-config.js index 15c5ad9..e0bfdf2 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -13,7 +13,7 @@ module.exports = { { resolve: `gatsby-plugin-gtag`, options: { - trackingId: process.env.GA_ID, + trackingId: "G-N2Q5X5JPZS", head: false, anonymize: true, },