코드를 완성하고 E2E 테스트를 돌렸는데.. 간단한 테스트에서도 통과를 못한다..?

sy u·2025년 4월 21일
0
post-thumbnail

TL;TD

  • 문제 발생
    • 개발 환경과 테스트 환경 모두 mock 데이터를 사용하도록 구성됨
    • Playwright 테스트 환경에서 기대한 테스트용 응답 대신 개발용 mock 응답이 반환되어 테스트가 통과하지 않음
  • 원인
    • Playwright에서 사용하는 page.route()는 Service Worker가 요청을 가로채면 작동하지 않음
    • 개발 환경에서 msw.worker.start()가 이미 실행되고 있어 Service Worker가 먼저 요청을 가로채고 있었음
    • 그로 인해 Playwright의 핸들러가 무시되고 개발용 응답이 반환됨
  • 결론
    • 로컬 테스트 실행 시 개발 서버를 끄지 않고 바로 테스트를 실행하면 예상치 못한 문제가 발생할 수 있음
    • MSW와 Playwright를 함께 사용할 땐 누가 요청을 먼저 가로채는지에 따라 결과가 달라질 수 있음
    • 설정을 제대로 이해하지 않으면 테스트 실패에 대한 디버깅에 시간 많이 소모할 수 있음

배경

2년 3개월간의 회사 생활을 마친 뒤 요즘 본격적으로 구직 활동에 집중하고 있습니다. 면접을 보기도 하고 과제나 코딩 테스트도 치르곤 하는데 과제를 제출하고 나면 항상 아쉬움이 남더라구요... 그래서 제출 후 다시 볼 수 없는 경우가 아니라면 과제는 한 번 더 풀어보면서 복습하는 편입니다. 처음 과제를 풀 때 놓쳤던 부분이나 더 나은 문제 해결 방식을 찾는 과정에서 인사이트를 얻고 있어요.

특히 이전에 풀었던 몇몇 과제들은 단위 테스트(Unit Test)나 E2E(End-to-End) 테스트 코드가 포함되어 있어, 다시 풀어본 본 후에 검증해볼 수 있었는데요. 최근 한 과제에서 다시 풀어본 과제의 테스트가 실패했다는 결과를 확인했음에도 불구하고 제 로컬 환경에서는 문제가 발생하지 않아 원인을 찾는 데 어려움을 겪기도 했습니다.

이 포스팅에서는 테스트에 오류가 발생한 오류의 원인을 파악하고 디버깅해나가는 과정에 대해 자세히 다뤄보고자 합니다. Playwright나 MSW, Service Worker에 대한 기본적인 설명은 생략하고 디버깅 과정과 오류 해결에 초점을 맞추어 관련 라이브러리의 내부 동작에 대해서만 담을 예정입니다.

로컬 개발환경에서는 잘 되는데 Playwright에선 왜 안 되지?

문제 발생 상황

(과제의 세부 내용은 공개할 수 없기 때문에 디버깅을 위한 예제를 별도로 만들어 검증해 보겠습니다.
테스트는 Playwright와 MSW를 사용하고 있습니다.)

검증을 위해 임의로 아래와 같이 개발용, 테스트용 mock data를 정의해보겠습니다.

// api
await axios.get<Test>("/api/test");
// 개발 mock data
[
  { id: 1, title: '나는 개발용이야' }
]
// 테스트 mock data
[
  { id: 2, title: '나는 테스트용이야' }
]

보다 효율적인 디버깅을 위해 UI 모드로 테스트를 실행하겠습니다.

(--debug 모드로 할 수도 있지만 저는 UI 모드가 편하더라구요.)

npx playwright test --ui

UI 모드에서는 테스트 중 오류가 발생했을 때 Errors 탭을 통해 오류 메시지를 확인할 수 있습니다.

제 경우에는 테스트 코드가 요구한 '나는 테스트용이야' 메시지가 아닌 '나는 개발용이야' 메시지가 표시되어 timeout error가 발생했던 것이 원인이었습니다 🥲.

디버깅

원인을 분석하기 위해 먼저 테스트 코드에서 요구하는 '나는 테스트용이야' 텍스트가 출력되는 조건과 관련된 테스트 코드를 살펴보겠습니다.

  • 테스트용 핸들러 적용
// tests/test.ts
test.beforeEach(async ({ worker }) => {
  await worker.use(...[테스트용 핸들러 목록]));
});
-  테스트가 실행되기 전 테스트용 mock 데이터를 반환하는 핸들러를 추가합니다.
    - 여기서 beforeEach는 각 테스트가 실행되기전 실행하는 hook입니다. 
  • '나는 테스트용이야' 텍스트 확인
// tests/test.ts
test.describe("테스트1", () => {
  test("나는 테스트용이야 텍스트 확인", async ({ page, worker }) => {
    await page.goto("/");
    await page.waitForLoadState("domcontentloaded");
	await expect(page.getByText("나는 테스트용이야")).toBeVisible();
    ...
  });
})
- 페이지에 진입하면 `/api/test GET` 요청이 발생하고 응답으로 받아온 텍스트가 화면에 표시됩니다.
- 이때 화면에 표시되는 텍스트가 실제로 "나는 테스트용이야"인지 확인하는 과정이 포함됩니다.

앞서 살펴본 내용을 기반으로 가장 의심스러웠던 부분은 worker에 핸들러가 정상적으로 등록되지 않았을 가능성입니다. 핸들러가 올바르게 설정되지 않았다면 테스트 실행 시 예상했던 응답이 제대로 반환되지 않아 원하는 텍스트가 화면에 표시되지 않을 수 있다고 생각했습니다. 이러한 가능성을 확인하기 위해 몇 가지 가설을 설정하고 이를 점검해 보겠습니다.

가설1: 테스트용 handler가 정상적으로 등록됐는가

  • beforeEach가 정상적으로 동작했는지 내부에 console 출력

    // tests/test.ts
    test.beforeEach(async ({ worker }) => {
      console.log('✅ 함수 실행됨!')
      await worker.use(...[테스트용 핸들러 목록])`);
    });
    • 결과: Console 탭에서 정상적으로 출력
  • page.on('response') 이벤트 리스너를 사용해서 API 요청이 테스트용 mock data를 응답 받았는지 확인

        page.on('response', async (response) => {
          if (response.url().includes('/api/test')) {
              try {
                  const json = await response.json();
                  console.log(`응답 데이터:`, json);
              } catch (error) {
                  console.log(`JSON 변환 실패:`, error);
              }
          }
        });
    • 결과: /api/test GET 요청은 개발용 moking data를 반환

실제로 /api/test 요청은 테스트용 mock 데이터가 아닌 개발용 mock 데이터를 반환하고 있었습니다.
그렇다면 worker에 handler가 등록되지 않은걸까요?

이 과정에서 console 탭에서 아래와 같은 에러 로그를 추가로 발견하게 됩니다.

위 에러는 worker.start()가 중복으로 호출되었다는 의미로 이미 MSW(Mock Service Worker)가 활성화된 상태라면 해당 호출을 제거해도 된다는 뜻입니다.
따라서 중복 호출이 실제 문제의 원인인지 추가적인 확인이 필요했습니다.

가설 2: worker가 중복 실행되서일까?

  • worker.start()가 호출되고 있는 곳

    import { setupWorker } from 'msw/browser'
    
    useEffect(() => {
        const worker = setupWorker(...[개발용 핸들러]);
        await worker.start();
    }, []);

    해당 프로젝트는 테스트 환경뿐 아니라 개발 환경에서도 MSW를 사용하고 있었기 때문에 최상위 컴포넌트의 useEffect에서 worker.start()를 호출하고 있었습니다.
    IDE를 통해 worker.start()를 검색해봐도 오직 이 곳만 검색이 되는데요.
    이 부분을 주석 처리하고 다시 테스트를 실행해 보겠습니다.

  • worker.start()를 주석 처리, 다시 테스트 실행

    네... worker.start()를 주석 처리 하니 테스트가 정상적으로 통과했습니다.

문제가 해결된 듯했지만 디버깅 과정에서 이해가 되지 않은 부분이 남아있습니다.

테스트 상황에서 worker.start() 호출로 인해 테스트용 핸들러가 아닌 개발용 handler가 적용된 이유는 무엇인지,
테스트용 핸들러를 정상적으로 적용해도 개발용 handler가 덮어씌워지는 것인지에 대한 추가적인 확인이 필요했습니다.

가설 3: handler 가 덮어 씌워지는 걸까?

이 가설을 살펴보기 위해서는 worker.start()가 어떻게 동작하는지, 각각 전달된 handler가 내부적으로 어떻게 관리되는지를 확인할 필요가 있다고 생각했습니다.

지금부터 내부 동작을 조금 더 자세히 살펴보겠습니다.

내부 동작 파헤치기 (삽질하기)

MSW의 worker.start()는 어떻게 동작할까?

Mock Service Worker (MSW)

MSW는 브라우저와 Node.js 환경을 위한 API 모킹 라이브러리입니다.

MSW를 사용하면 네트워크 요청을 가로채고(intercept) 관찰하며 정의된 모의 응답(mock response)을 제공할 수 있습니다.

setupWorker()

브라우저 환경에서 API 모킹이 가능하도록 client-worker 통신 채널을 설정합니다.

import { setupWorker } from 'msw/browser'
import handlers from './handlers'
 
const worker = setupWorker(...handlers)

위 예시 코드와 같이 setupWorker에 여러 개의 handler를 스프레드 문법(...)으로 전달할 수 있습니다.

setupWorker() 함수는 현재 브라우저 환경에서 API 모킹을 제어하는 데 사용할 수 있는 객체인 Worker 인스턴스를 반환합니다.

생성된 Worker는 다양한 메서드를 제공합니다. 특히 우리가 살펴볼 대상인 start() 메서드 또한 포함하고 있습니다.

이제 본격적으로 worker.start()가 어떻게 동작하는지 확인해 보겠습니다.

worker.start()

worker.start()를 호출하면 Service Worker를 등록하고 reqeust를 가로채기 시작합니다.

기본으로 서비스 워커 스크립트가 특정 경로(./mockServiceWorker.js)에서 제공되며 필요 시 사용자가 원하는 custom 경로를 옵션으로 지정할 수 있습니다.

worker.start({
  serviceWorker: {
    url: '/assets/mockServiceWorker.js', //custom path
  },
})
worker.start 내부 구현
  • createStartHandler

    // msw/createStartHandler.ts
    
    export const createStartHandler = (
      context: SetupWorkerInternalContext,
    ): StartHandler => {
      return function start(options, customOptions) {
        ...
        // Handle requests signaled by the worker.
          context.workerChannel.on(
            'REQUEST',
            createRequestListener(context, options),
          )
    
          // Handle responses signaled by the worker.
          context.workerChannel.on('RESPONSE', createResponseListener(context))
        ...
      }
    }
    • start()는 createStartHandler에서 반환된 함수입니다.
    • Service Worker가 요청을 가로채면 createRequestListener()에서 반환된 함수가 실행됩니다.
  • createRequestListener

    export const createRequestListener = (
      context: SetupWorkerInternalContext,
      options: RequiredDeep<StartOptions>,
    ) => {
      return async (
        event: MessageEvent,
        message: ServiceWorkerMessage<
        'REQUEST',
        ServiceWorkerIncomingEventsMap['REQUEST']
        >,
          ) => {
          ...
          await handleRequest(
            ...
          )
          ...
        }
    }
    • createRequestListener는 내부에서 handleRequest 함수를 호출합니다.
    • handler가 정의되지 않은 request에 대해서는 NOT_FOUND 메시지를 서비스 워커로 전달합니다.
    • 핸들러가 존재할 경, 요청에 맞는 가상 응답(mock response)을 생성하여 서비스 워커로 다시 전달합니다.

즉, worker.start()가 실행되면 Service Worker request를 가로채고 내부에서는 해당 요청을 처리할 수 있는 핸들러를 찾고 처리한 결과를 다시 Service Worker로 되돌려줍니다.

그렇다면 Playwright를 통해 MSW를 사용할 때 실제로 요청에 대한 핸들러가 전달되는 과정은 어떻게 이루어질까요? 이에 대한 흐름을 살펴보겠습니다.

Playwright에서 start()의 역할을 하는 부분은 어디일까?

playwright-msw

playwright-msw는 Playwright 테스트 환경에서 API 요청을 가로채고 모의 응답(mock response)을 제공할 수 있게 해주는 라이브러리입니다.
Playwright의 기본 테스트 기능을 확장하는 방식으로 API 요청을 가로채는 Mock Server를 설정할 수 있습니다.

createWorkerFixture(handlers, config)

createWorkerFixture()는 테스트가 실행될 때마다 새로운 모킹 환경을 설정하는 Fixture를 반환하는 함수입니다. (공식 설명)
playwright의 기본 테스트 기능을 확장해, API 요청을 가로채는 Mock Server를 설정할 수 있습니다.
createWorkerFixture 함수에 handlers(테스트용)를 전달합니다.

*️⃣ Fixture
Fixture는 Playwright Test에서 각 테스트가 실행되기 전에 필요한 환경을 설정하고 테스트 종료 후 정리(clean up)까지 자동으로 수행하는 역할을 합니다. (공식 설명)

// 사용 예시
import { test as base, expect } from '@playwright/test';
import { http } from 'msw';
import type { MockServiceWorker } from 'playwright-msw';
import { createWorkerFixture } from 'playwright-msw';

import handlers from './handlers';

const test = base.extend<{
  worker: MockServiceWorker;
  http: typeof http;
}>({
  worker: createWorkerFixture(handlers),
  http
});

export { expect, test };
createWorkerFixture 내부 구현
  • createWorkerFixture
    // playwright-msw/lib/fixture.ts
    const createWorkerFixture = (handlers = [], config) => [
        ({ page }, use) => __awaiter(void 0, void 0, void 0, function* () {
            const worker = yield (0, worker_1.createWorker)(page, handlers, config);
            ...
    ];
    exports.createWorkerFixture = createWorkerFixture;
    • 전달받은 handlers를 createWorker 함수로 전달합니다.
  • createWorker
        const createWorker = (page, requestHandlers, config) => __awaiter(void 0, void 0, void 0, function* () {
            const router = new router_1.Router(page, requestHandlers, config);
            yield router.start();
            return {
                use: (...handlers) => __awaiter(void 0, void 0, void 0, function* () { return router.use(...handlers); }),
                resetHandlers: (...handlers) => __awaiter(void 0, void 0, void 0, function* () { return router.resetHandlers(...handlers); }),
                resetCookieStore: () => {
                    cookies_1.store.clear();
                },
            };
        });
        exports.createWor ker = createWorker;
    • Playwright 개별 page의 API 요청을 가로채는 Mock Server를 생성하고 MockServiceWorker 객체를 반환합니다.
    • 내부적으로 Router 인스턴스를 생성하고 초기화(router.start())합니다.
  • Router.start
        class Router {
            ...
            start() {
                return __awaiter(this, void 0, void 0, function* () {
                    ...
                    for (const initialHandler of this.initialRequestHandlers) {
                        yield this.registerMswHandler(initialHandler);
                    }
                    ...
                });
            }
            ...
        }
    • registerMswHandler를 호출하여 전달받은 모든 핸들러를 등록합니다
  • Router.registerMswHandler
        class Router {
            ...
            registerMswHandler(handler) {
                return __awaiter(this, void 0, void 0, function* () {
                    const path = (0, utils_1.getHandlerPath)(handler, this.config);
                    ...
                    this.setRouteData({
                        path,
                        routeHandler: yield this.registerPlaywrightRoute(path),
                        requestHandlers: [handler],
                    });
                    ...
                });
            }
            ...
        }
    • getHandlerPath를 사용하여 handler가 처리할 request 경로(path)를 추출합니다.
    • 추출한 경로는 registerPlaywrightRoute 메서드로 전달됩니다.
  • Router.registerPlaywrightRoute
        class Router {
            ...
            registerPlaywrightRoute(path) {
                return __awaiter(this, void 0, void 0, function* () {
                    const routeHandler = (route) => {
                        ...
                        return (0, handler_1.handleRoute)(route, requestHandlers);
                    };
                    yield this.page.route((0, utils_1.convertMswPathToPlaywrightUrl)(path), routeHandler);
                    ...
                });
            }
            ...
        }
    • 전달받은 경로에 대해 QPlaywright의 라우팅 기능을 설정하고 해당 경로로 들어오는 요청을 처리하는 handler(routeHandler)를 등록 및 반환합니다.
      • routeHandler
        • 라우팅 경로와 매칭되는 요청을 처리하는 역할을 수행합니다.
        • handleRoute 함수를 호출하는데 이때 현재 route와 등록된 핸들러들을 매개변수로 전달합니다.
  • handleRoute
        ...
        const handleRoute = (route, handlers) => __awaiter(void 0, void 0, void 0, function* () {
            ...
            yield (0, msw_1.handleRequest)(...)
            ...
        }
        ...
    • MSW 함수인 handleRequest를 호출하여 전달된 handler들을 통해 요청을 처리합니다.
      • 이 handleRequest 는 우리가 MSW의 worker.start() 내부 구현을 확인할 때 봤었던 handleRequest 동일한 함수입니다.
    • setRouteData에 path와 routeHandler, requestHandlers를 매개변수로 전달합니다.
  • Router.setRouteData
    • 전달 받은 데이터를 this.routes 객체에 설정합니다.

결국 우리가 전달한 테스트용 핸들러들은 plarywright-msw의 Router 인스턴스 내부에서 관리되며 MSW의 handleRequest를 통해 Playwright 페이지의 요청을 적절히 처리하고 모킹된 응답을 제공하는 것이죠.

공통적으로 쓰이는 handleRequest의 정체는?

handleRequest는 요청(request)을 받아 적절한 응답(response)을 결정하는 핵심 로직입니다.
전달받은
매개변수로 받은 핸들러중에서 요청을 처리할 핸들러가 있는지 확인하고 핸들러가 존재하면 onMockedResponse 함수를 호출하여 응답을 처리합니다.

즉 playwright-msw와 MSW 둘 다 내부에서 handleRequest를 호출하고 있지만 handleRequest는 내부에서 핸들러를 저장하고 관리하는 것이 아니라 전달받은 핸들러로 요청을 처리하는 역할만 합니다. 결국 서로 독립적으로 핸들러를 관리하기 때문에 위에서 세웠던 가설3 마저 아닌 것으로 확인했습니다.

다시 디버깅

가설4: handler가 모두 정상적으로 등록됐다면 네트워크의 Request를 가로채는 순서나 우선순위의 문제일까?

API 요청 가로채기 방식 비교: MSW vs playwright-msw

MSW의 가로채기 방식
  1. worker.start()를 호출하여 브라우저에 Service Worker를 등록합니다.
  2. 이후 발생하는 모든 네트워크 요청을 감시하고 가로챌 준비를 합니다.
  3. 브라우저에서 API 요청(fetch('/api/user'))이 발생합니다.
  4. Service Worker가 요청을 가로채고 request 데이터를 메시지를 통해 MSW로 전달합니다.
  5. MSW는 내부적으로 request url과 매칭되는 핸들러를 찾습니다.
  6. 조건에 부합하는 핸들러를 실행하여 Response 객체를 생성합니다.
  7. 생성된 Response는 다시 Service Worker로 전송됩니다.
  8. 최종적으로 브라우저는 서버에서 직접 받은 응답처럼 해당 Response 객체를 수신합니다.
playwright-msw의 가로채기 방식
  1. playwright의 테스트 객체를 확장을 통해 createWorkerFixture()를 호출해 MSW에 핸들러를 등록합니다.
  2. 등록된 핸들러는 내부적으로 Router 객체에 저장됩니다.
  3. Playwright 테스트가 실행되면 테스트 브라우저(page)가 활성화됩니다.
  4. 이 때 page.route(...)는 모든 등록된 핸들러 모든 경로를 대상으로 요청을 가로챌 준비를 합니다.
  5. 브라우저에서 API 요청(fetch('/api/user'))이 발생합니다.
  6. Playwright의 page.route()가 요청을 가로챕니다.
  7. 요청과 매칭되는 핸들러를 찾습니다.
  8. 조건에 부합하는 핸들러를 실행하여 Response 객체를 생성합니다.
  9. Playwright의 route.fulfill() 메서드를 통해 브라우저에 직접 응답합니다.

MSW와 다르게 Service Worker를 사용하지 않고 오직 Playwright의 단독 기능(page.route)으로만 요청을 가로챕니다.

중요한 점은 msw와 다르게 playwright는 page.route로 요청을 가로 챈다는 건데요 page.route를 알아보는 도중 공식홈페이지(page.route)에서 중요한 글을 발견하게 됩니다.

문제 원인 발견

공식홈페이지(page.route)

NOTE
page.route() will not intercept requests intercepted by Service Worker. See this issue. We recommend disabling Service Workers when using request interception by setting serviceWorkers to 'block'.

Playwright 공식 문서에 따르면 Service Worker가 request를 가로채면 playwright의 page.route는 해당 request를 볼 수 없어 가로챌 수 없다는 것입니다...

눈으로 직접 확인하기 위해 Service Worker가 등록됐을때와 아닐 때 console.log를 통해 출력해 보겠습니다.

    async _onRoute(route) {
        console.log('가로챈 요청 URL:', route.request().url())
        ...
    } 

위 코드는 page.route가 요청을 가로 챘을때 실행되는 callback 함수입니다.

  • Service Worker가 등록되지 않았을 때(msw의 worker.start()를 주석 처리)

    • 정상적으로 요청이 가로채진 것으로 확인됩니다.
  • Service Worker가 등록됐을 때(msw의 worker.start() 실행)

    • 가로채진 요청이 없어 _onRoute 자체가 실행되지 않았습니다.

결국 Service Worker가 request를 가로채면 Playwright의 page.route는 해당 request를 볼 수 없어 가로채지 못하고 등록된 handler를 반환할 수 없는 것이죠.

그럼 어떻게 이 상황을 해결할 수 있을까요?
테스트 환경에서 Service Worker를 등록하지 않기 위해 항상 worker.start()를 주석처리 해야 하는 걸까요?

문제 해결

테스트 환경에서 항상 worker.start()를 비활성화 할 필요는 없습니다. 대신 상황에 따라 조건부로 분기 처리하거나 Playwright의 설정을 활용하여 해결할 수 있습니다.

방법1: 환경 변수를 이용한 조건부 분기 처리

  • package.json에 테스트용 환경변수 설정
        // package.json
        {
            ...
            scripts: {
                "dev:test": "TEST=true"
            }
            ...
        }
  • 환경변수 TEST가 true로 설정되어 있을 경우 work.start()가 실행되지 않도록 조건문 추가
        if (typeof window !== 'undefiend' || process.env.TEST === 'true') {
            const worker = setupWorker(...handlers);
            worker.start();
        }
  • 실행된 개발서버를 종료
  • Plarywright 테스트를 실행

방법2: Playwright 설정(webServer, use) 활용

option 설명

  • webServer

    • 테스트를 실행하기 전에 로컬 개발 서버를 실행할 수 있는 옵션입니다.

          // playwright.config.ts
      
          import { defineConfig } from '@playwright/test';
      
          export default defineConfig({
          // Run your local dev server before starting the tests
              webServer: {
                  command: 'npm run dev',
                  url: 'http://localhost:3000',
                  reuseExistingServer: !process.env.CI,
              },
          });
    • command

      • 로컬 개발서버를 실행할 명령
    • url

      • 서버가 제대로 실행됐는지 확인하려고 요청 보내는 주소
    • reuseExistingServer

      • 포트에 이미 서버가 실행 중이면 새로운 인스턴스를 실행하지 않고 기존 서버를 재사용할지 여부
      • 보통 !process.env.CI을 값으로 사용
        • CI 환경에서는 false로 설정되어 테스트를 실행하면 항상 새로운 인스턴스를 실행하도록 하는 것이죠.
  • use

    • 브라우저 종류, 뷰포트 크기, 로그인 상태, 쿠키 등 테스트 환경을 미리 세팅할 수 있는 옵션입니다.

          // playwright.config.ts
      
          import { defineConfig } from '@playwright/test';
      
          export default defineConfig({
              use: {
                  // Base URL to use in actions like `await page.goto('/')`.
                  baseURL: 'http://localhost:3000',
              },
          });
    • baseUrl

      • 테스트에서 사용하는 기본 URL

방법

  • package.json에 테스트 환경을 위한 환경변수를 설정 (방법1과 동일)

  • webServer 설정을 통해 별도 포트로 개발 서버를 실행

        // playwright.config.ts
    
        import { defineConfig } from '@playwright/test';
    
        export default defineConfig({
            webServer: {
                command: 'npm run dev:test', // 여기를 수정
                url: 'http://localhost:4000',
                reuseExistingServer: !process.env.CI,
            },
        });
    
  • use.baseUrl을 webServer.url과 동일하게 설정

        // playwright.config.ts
    
        import { defineConfig } from '@playwright/test';
    
        export default defineConfig({
            use: {
                baseURL: 'http://localhost:4000',
            },
        });
    
  • 실행된 개발서버를 종료하지 않아도 됨

  • Plarywright 테스트를 실행

결론

사실 진행했던 과제에는 이미 해결 방법1이 적용되어 있었습니다. 그리고 PR을 올린 후 CI 환경에서 자동으로 테스트가 실행될 때에는 문제가 되지 않습니다. (즉 제출에는 아무 문제가 없었습니다.)

다만 로컬에서 테스트를 실행하고 싶었던 저는 README.md 파일만 참고하여 테스트 실행 방법을 확인했고 별도로 package.json 파일을 확인하지 않다 보니 테스트 전용 로컬 서버를 실행하는 스크립트가 있다는 점을 미처 인지하지 못했습니다. 그 결과 로컬 서버를 종료하지 않은 상태에서 테스트를 실행하여 문제가 발생했던 것이죠 🥲

이미 Playwright를 활용해 테스트 하는 것에 능숙했다면 조금 더 쉽게 원인을 파악하고 해결할 수 있었겠지만 아직 Playwright 사용이 능숙하지 않았던 저에게는 라이브러리 내부를 살펴볼 수 있는 좋은 경험이었던 거 같습니다.

짧은 시간 내에 과제를 수행해야 하는 상황에서는 이러한 간단한 문제로 인해 혼란을 겪을 수 있다고 생각합니다. 저와 비슷한 경험을 하시는 다른 분들이 이 글을 통해 빠르게 원인을 파악하고 해결하실 수 있었으면 좋겠습니다.

0개의 댓글