PlayWright를 이용한 웹앱 테스트

플레이라이트는 웹앱에 대한 테스트 자동화 라이브러리로 JS 코드(또는 TS)로 테스트를 작성할 수 있다. 바이브 코딩 시 기능에 대한 테스트를 AI 통해 하고자할때 매우 유용한데, 웹브라우저의 특정 버튼을 AI가 클릭하고 그 결과가 기대한 것와 같은지에 대한 인터렉티브한 행동 테스트도 가능하다. 우와, 몇달 전까지만 해도 이런건 사람의 영역이라 생각했다. 하나씩 하나씩 인간의 만용을 AI는 박살내고 있다. 겸손 겸손…

플레이라이트의 목적이 특정 웹앱에 대한 테스트에 있다. 즉, 테스트를 위한 독립된 프로젝트를 구성할 수 있다. 명령은 다음과 같다.

npm init playwright@latest

위의 명령이 실행되면 인간은 다음과 같은 내용을 입력해야 한다. (음.. AI를 에이전트라고 부른다면 인간은 어떻게 불러야 구분이 될까? 개발자라고 하기엔 AI 역시 코딩을 작성하므로 개발자가 아닌게 아니니..)

Initializing project in '.'
✔ Do you want to use TypeScript or JavaScript? · TypeScript
✔ Where to put your end-to-end tests? · tests
✔ Add a GitHub Actions workflow? (Y/n) · false
✔ Install Playwright browsers (can be done manually via 'npx playwright install')? (Y/n) · true

테스트 프로젝트를 위한 파일들이 자동으로 구성되는데, 여기서 테스트를 위한 코드 작성은 tests 폴더(앞서 인간이 입력한 값)에 example.spec.ts 파일이 보이는데, 그냥 이 파일에 테스트 코드를 입력하면 된다. 이미 테스트로 잘 잘동하는 테스트 코드가 있는데, 그건 지우고 다음과 같은 코드를 작성해 테스트해 보았다.

import { test, expect } from '@playwright/test';
import { chromium } from '@playwright/test';

test('login', async () => {
  const browser = await chromium.launch({
    headless: false,
    args: [
      '--enable-unsafe-webgpu',
      '--enable-features=Vulkan'
    ]
  });

  const context = await browser.newContext();
  const page = await context.newPage();

  await page.goto('https://www.hyyyydai-3d.com');

  await page.fill('.user-id input', 'gizmo');  
  await page.fill('.user-pw input', '*********');

  await page.locator('.user-id input').press('Tab');

  await page.click('.btn-login');

  await expect(page.getByText('공지사항')).toBeVisible();
});

위의 테스트는 딱 1개인데, 주어진 url에 들어가서 id와 암호를 자동으로 채우고 로그인 버튼을 클릭해서 ‘공지사항’이라는 텍스트가 보이면 테스트 성공이라는 것이다.

내가 테스트해야할 웹 페이지가 3D 그래픽인지라 WebGPU에 대한 활성화 없이는 테스트에 실패함으로, 위의 코드에는 WebGPU에 대한 활성화 코드가 있고 테스트가 잘되는것을 확인할 수 있었다. 실제 테스트 실행은 다음과 같다.

npx playwright test

테스트를 위한 전용 웹브라우저를 보면서 테스트가 진행되는 것을 살펴보고 싶은 꼼꼼이라면 (대신 사는게 고달프겠지.. 나처럼 ㅜ_ㅜ) 다음과 같은 방식의 테스트 실행도 가능하다.

npx playwright test --ui

이 글은 플레이라이트를 처음 사용하는 인간을 위한 가이드일뿐이다. 보다 상세한 방법은 AI에게 물어보고 가이드 받으라.

Mermaid 문법

```mermaid
graph TD
    A[사각박스] --> B{다이아몬드박스}
    B -->|라벨1| C((원형박스))
    B -->|라벨2| D(라운드박스)
    C --> E([텍스트])
    subgraph 서브그래프
        D --> E
        E --> F[[서브루틴]]
        E --> G[(저장소박스)]
    end
    E --> H([경기장박스])
    E --> I>리본박스]
    E --> J[/기울박스1/]
    E --> K[\기울박스2\]
    J ---|실선| K
    A -.->|점선| F
    G ==>|굵은화살| H
    F ===|굵은실선| H
    K --> L{{육각박스}}
```

```mermaid
classDiagram

class 클래스명 {
    +공용_필드: string
    -비공개_필드
    #보호된_필드
    ~패키지_내부_필드
    +공용_메서드(a: int): void
    -비공개_메서드()
    #보호된_메서드()
    ~패키지_내부_매서드()
}
```

```mermaid
classDiagram
    class 오리 {}
    class 물고기
    class 얼룩말 {
        <<abstract>>
    }
    class 동물 {
        <<interface>>
    }
    class 닭
    class 날개
    
    동물 <-- 오리
    동물 <.. 물고기
    동물 <|-- 얼룩말
    동물 <|.. 닭
    얼룩말 -- 물고기
    오리 *--> 날개
    닭 *-->"2" 날개
    비늘"*"--"1"물고기
```

Mermaid 참조

npm으로 설치된 모든 패키지를 최신 버전으로 업데이트

npm 방식임. 먼저 npm-check-updates를 전역적으로다가 설치(설치 없이 바로 실행하는 방법은 맨 하단 참조)

npm install -g npm-check-updates

이미 설치된 패키지가 담긴 node_modules 폴더와 패키지 고정 버전 정보가 담긴 package-lock.json 파일을 삭제 후 다음 명령 입력 …

ncu -u

package.json에 최신 버전의 패키지가 반영되고 이제 npm i로 설치하면 끝.

설치없이 바로 실행은 아래 참조 …

npx npm-check-updates -u

드디어 three.js에서도 GI 기능이 지원됩니다.

드디어 three.js의 WebGPU 지원이 매우 안정화되면서 GI(Global Illumination, 전역 조명) 기능을 사용할 수 있습니다. 꽤 오래전 babylon.js에서 GI 기능을 제공하지만 three.js는 그러지 못해 늘 안타까웠는데요. 먼저 GI란 3D 장면 내에서 빛이 광원에서 나와 물체에 직접 닿는 것(직접광)뿐만 아니라, 표면에 반사되고 산란되어 주변 환경을 밝히는 간접광(Indirect Lighting)까지 모두 계산하여 사실적인 조명 효과를 만들어내는 기술이라고 정의되어 있습니다. 네, 더 나은 장면을 만들어 주는 기술이라는 것입니다. 먼저 GI가 적용되지 않은 장면입니다. 광원은 PointLight 하나만 사용되었습니다.

오직 PointLight에 대한 광원만으로 개별 메시에 대한 쉐이딩만이 고려되어 렌더링된 일반적인 three.js 렌러링 결과죠. 부족한 이런 렌더링 품질을 보완하기 위해 블렌더와 같은 3D 툴을 이용해 광원맵을 baking해서 사용하곤 했습니다. 하지만 이제 그럴 필요없이 GI를 적용하면 위의 결과가 아래처럼 바뀝니다.

품질이 훨씬 좋아졌습니다. FPS가 양호합니다. 예전에 babylon.js에서 GI를 적용했을때 FPS가 매우 나쁘게 나왔는데 three.js는 양호합니다. 물론 현재 babylon.js의 GI 기능이 개선되어 좋아졌을 수도 있지만 말입니다.

끝으로 주요 GI 기술의 종류는 3가지입니다.

  • 레이 트레이싱(Ray Tracing): 빛의 경로를 역추적하여 반사, 굴절 등을 물리적으로 정확하게 계산.
  • 라이트 맵(Light Map): 빛의 효과를 미리 계산해 텍스처로 물체에 입히는 방식.
  • 앰비언트 오클루전(AO): 주로 구석이나 틈새 부분에 은은한 음영을 추가해 3D 모델의 입체감을 향상시키는 기술 (GI의 보조 역할).

이제 넓은 범위의 지형 지물에 대한 장면에 GI를 적용해 멋진 장면을 렌더링해 봅시다.

TSL에서 법선벡터와 관련된 노드 함수에 대한 고찰

fragmentNode에서 사용할 수 있는 일반적인 법선벡터는 normalWorld 노드이다. normalWorld은 이미 normalize가 되어 있다. transform이 전혀 이뤄지지 않았을지라도 normalLocal은 normalWorld와 다르며 normalLocal을 정규화해야 비로써 normalWorld와 같아진다. 즉, normalWorld.sub(normalLocal)은 0 벡터가 아니며 normalWorld.sub(normalLocal.normalize())가 0 벡터라는 것인데, 전제 조건은 transform이 전혀 이뤄지지 않았을때이다. normalWorld를 직접 계산해 보면 다음과 같다.

const normalView = vertexStage( modelNormalMatrix.mul( normalLocal ) );

vertexStage 노드는 vertex shader에서 varying(보간)으로 fragment로 넘겨준다. varying으로 넘겨줬고 법선벡터는 단위벡터로 사용되어야 하므로 넘겨받은 fragment 측에서 반드시 정규화를 시켜야 한다. 즉, normalWorld와 직접 계산한 normalView가 동일한 값을 가지려면 normalView를 정규화해야 한다.

가끔 시각화를 통해 normal의 동일성 여부를 확인하려고 할때가 있다. 즉, 아래의 코드처럼 말이다.

material.fragmentNode = Fn(([]) => {
  const color = normalWorld.sub(normalLocal);
  return vec4(color, 1);
})();

결과는 마치 normalWorld와 normalLocal이 동일하기라도 한것처럼 까맣게 표시된다. 하지만 아니다. 위의 코드 중 normalWorld.sub(normalLocal)의 결과값에 1000정도 곱해줘 값을 증폭시켜 보면 다음처럼 값에 대한 차이값을 눈으로 볼 수 있다.

쉐이더 프로그래밍의 디버깅은 이처럼 사람의 눈으로 직접 확인하는 방법 이외에 뾰족한 수가 없다는 문제가 있는데, 위와 같은 상황도 있다는 것을 미리 알아두면 좋을 것이다.