소스 정보
- 저장소
- puk0806/gugbab-claude
- 최근 소스 활동
- 2026년 8월 26일 08:04
- 감지된 SKILL.md 언어
- 한국어
- 스타
- 2
- 포크
- 0
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
메뉴
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/puk0806/gugbab-claude --skill e2e-testing명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SKILL.md 표시 중
DDD(Domain-Driven Design) 아키텍처 핵심 패턴 - 유비쿼터스 언어, 서브도메인, 바운디드 컨텍스트, Aggregate, Entity/VO, 도메인 서비스/이벤트, 레이어드 아키텍처
대규모 React/Next.js 프로젝트를 layer-first(types/·utils/·hooks/·api/·components/ 밑에 도메인이 반복되는 구조)에서 domain-first(feature/도메인 우선) 구조로 전환하는 설계 기준과 절차. Feature-Sliced Design 2.1 정본(layers 6종·slices·segments·import 규칙·@x 크로스임포트·public API), FSD를 쓰지 않는 경량 대안(features + shared 2~3계층 + ESLint import/no-restricted-paths), Next.js App Router 공존 전략(route group `()`·private folder `_`·colocation), Turborepo/Nx 모노레포에서 폴더↔패키지 승격 기준, colocation과 배럴 파일 성능 트레이드오프, 도메인 경계 역추출(import 그래프·change coupling·용어 클러스터), 전환 실패 패턴(shared 비대화·entities 남용·순환 의존·도메인=라우트 착각·조기 추상화). 도메인 개념 자체(바운디드 컨텍스트·유비쿼터스 언어)는 `architecture/ddd` 스킬을 참조한다.
소스 파일 수천 개 규모 프론트엔드 코드베이스를 멈추지 않고 점진 재구조화하는 실행 전략 - Strangler Fig / Branch by Abstraction / Parallel Change, ts-morph·jscodeshift codemod, PR 분할·검증 게이트·되돌리기, 테스트 없는 코드의 안전망, 작업 순서 설계와 위반 수 기반 진행 추적
| name | e2e-testing |
| description | Playwright E2E 테스트 — 설치, 설정, 로케이터, POM, 네트워크 모킹, 인증, 비주얼 회귀, CI, 디버깅 전략 |
소스: https://playwright.dev/docs/intro | https://playwright.dev/docs/best-practices 검증일: 2026-08-26 (최초 2026-04-20 · 08-26 freshness 재검증: 로케이터 우선순위·
page.route·defineConfig·fixture 확장 전부 현행, 버전 문자열만 갱신) 대상 버전: Playwright v1.62.x (최신 안정, 2026-08 기준)
# 신규 프로젝트 초기화 (권장)
npm init playwright@latest
# 기존 프로젝트에 추가
npm install -D @playwright/test
npx playwright install --with-deps
초기화 시 생성되는 파일:
playwright.config.ts — 테스트 설정tests/example.spec.ts — 예시 테스트tests-examples/ — 추가 예제.github/workflows/playwright.yml — CI 워크플로 (선택)import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI, // CI에서 test.only 방지
retries: process.env.CI ? 2 : 0, // CI에서만 재시도
workers: process.env.CI ? 2 : undefined,
reporter: [
['html'],
['list'],
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry', // 실패 시 트레이스 수집
screenshot: 'only-on-failure',
},
projects: [
// 인증 셋업 프로젝트
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
dependencies: ['setup'],
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
dependencies: ['setup'],
},
// 모바일 뷰포트
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
dependencies: ['setup'],
},
],
// 개발 서버 자동 실행
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
},
import { test, expect } from '@playwright/test';
test.describe('로그인 플로우', () => {
test('유효한 자격 증명으로 로그인 성공', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('이메일').fill('user@example.com');
await page.getByLabel('비밀번호').fill('password123');
await page.getByRole('button', { name: '로그인' }).click();
// Web-first assertion — 조건 충족까지 자동 재시도 (기본 5초)
await expect(page).toHaveURL('/dashboard');
await expect(page.getByRole('heading', { name: '대시보드' })).toBeVisible();
});
test('잘못된 비밀번호로 에러 표시', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('이메일').fill();
page.().();
page.(, { : }).();
(page.()).();
});
});
여러 검증을 한 번에 수집하여 모든 실패를 한꺼번에 확인:
test('프로필 페이지 요소 확인', async ({ page }) => {
await page.goto('/profile');
// 실패해도 테스트를 중단하지 않음
await expect.soft(page.getByText('이름')).toBeVisible();
await expect.soft(page.getByText('이메일')).toBeVisible();
await expect.soft(page.getByText('가입일')).toBeVisible();
});
| 순위 | 로케이터 | 용도 |
|---|---|---|
| 1 | getByRole | ARIA 역할 기반 (접근성과 일치, 가장 견고) |
| 2 | getByLabel | 폼 필드 (label과 연결된 input) |
| 3 | getByPlaceholder | placeholder가 유일한 식별자일 때 |
| 4 | getByText | 텍스트 콘텐츠로 식별 |
| 5 | getByTestId | 시맨틱 식별이 불가능할 때 (data-testid) |
// 특정 리스트 아이템 내의 버튼 클릭
await page
.getByRole('listitem')
.filter({ hasText: '상품 A' })
.getByRole('button', { name: '삭제' })
.click();
// 나쁨 — CSS 선택자 (구현 종속, 리팩터링에 깨짐)
page.locator('.btn-primary.login-btn');
// 나쁨 — XPath (가독성 저하, 유지보수 어려움)
page.locator('//button[@class="submit"]');
// 좋음 — 사용자가 보는 것과 동일
page.getByRole('button', { name: '로그인' });
// e2e/pages/LoginPage.ts
import { type Page, type Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('이메일');
this.passwordInput = page.getByLabel('비밀번호');
this.submitButton = page.getByRole('button', { name: '로그인' });
this.errorMessage = page.getByRole('alert');
}
async goto() {
..();
}
() {
..(email);
..(password);
..();
}
}
// e2e/tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('로그인 성공', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await expect(page).toHaveURL('/dashboard');
});
// e2e/fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
type MyFixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
};
export const test = base.extend<MyFixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
});
export { expect } from '@playwright/test';
// e2e/tests/login.spec.ts
import { test, expect } from '../fixtures';
test('로그인 성공', async ({ loginPage, page }) => {
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await expect(page).toHaveURL('/dashboard');
});
test('상품 목록 표시', async ({ page }) => {
// API 응답을 가로채서 목 데이터 반환
await page.route('**/api/products', async (route) => {
await route.fulfill({
status: 200,
json: [
{ id: 1, name: '상품 A', price: 10000 },
{ id: 2, name: '상품 B', price: 20000 },
],
});
});
await page.goto('/products');
await expect(page.getByText('상품 A')).toBeVisible();
await expect(page.getByText('상품 B')).toBeVisible();
});
test('가격에 할인 적용', async ({ page }) => {
await page.route('**/api/products', async (route) => {
const response = await route.fetch();
const json = await response.json();
// 가격에 50% 할인 적용
const discounted = json.map((p: any) => ({
...p,
price: p.price * 0.5,
}));
await route.fulfill({ response, json: discounted });
});
await page.goto('/products');
});
// 1단계: HAR 녹화 (update: true)
test('API 응답 녹화', async ({ page }) => {
await page.routeFromHAR('e2e/fixtures/products.har', {
url: '**/api/products',
update: true, // 실제 API 호출하여 HAR 파일 생성
});
await page.goto('/products');
});
// 2단계: HAR 재생 (update 제거)
test('녹화된 API로 테스트', async ({ page }) => {
await page.routeFromHAR('e2e/fixtures/products.har', {
url: '**/api/products',
// update 없음 -> HAR 파일에서 응답 서빙
});
await page.goto('/products');
await expect(page.getByText('상품 A')).toBeVisible();
});
상세 레퍼런스 (예제·고급 패턴·흔한 실수) →
references/REFERENCE.md