jtwjs Dev Wiki
  • DEV_ROAD
    • 💪🏻 생존하기
    • Week 1
      • 개발 환경 세팅
      • 타입스크립트
      • 리엑트
      • Testing Library
      • Parcel & ESLint
    • Week 2
      • JSX
      • Virtual DOM
    • Week 3
      • React Component
      • React State
    • Week 4
      • Express
      • Fetch API & CORS
      • React Hook
      • useRef & Custom Hook
    • Week 5
      • TDD
      • React Testing Library
      • MSW
      • Playwrite
      • Snapshot
    • Week 6
      • Separtion of Concerns
      • Principle
      • DI, (Dependency Injection)
      • Reflect-metadata
      • TSyringe
      • External Store
      • Follow Redux
      • usestore-ts
      • useSyncExternalStore
    • Week 7
      • Routing
      • Routes
      • Router
      • Navigation
    • Week 8
      • Design System
      • Style Basics
      • CSS-in-JS
      • Styled-Components
      • Global Style & Theme
    • Week 9
      • 개발하기 전 준비
      • 상품 목록 페이지
      • 상품 상세 페이지
      • 장바구니 페이지
    • Week 10
      • 로그인
      • 로그아웃
      • 회원가입
      • 주문 목록 & 주문 상세
    • Week 11
      • 배송 정보 입력
      • 포트원 결제 요청
      • 배송 및 결제 정보 전달
    • Week 12
      • 관리자 웹사이트개발시작
  • DEV_NOTE
    • TypeScript
      • 기본적 문법
        • Enum
        • 다형성
          • Untitled
        • 구조적 타이핑
        • 제너릭 타입
        • 컨디셔널 타입
        • 함수 메서드 타이핑
        • infer로 타입스크립트의 추론 직접 활용
        • 재귀 타입
        • 템플릿 리터럴 타입
        • 추가적인 타입 검사 satisfies 연산자
        • 타입스크립트 건망증
        • 원시 자료형에도 브랜딩 기법 사용 가능
        • 타입 좁히기
        • 유용한 타입 만들기
        • 데코레이터 함수
        • 앰비언트 선언도 선언 병합이 된다.
        • 앰비언트 선언도 선언병합이 된다.
    • Testing
      • Unit Testing
      • 단위 테스트의 두 분파
      • 좋은 단위 테스트를 구성하는 4대 요소
      • 테스트 대역과 식별할 수 있는 동작
      • 단위 테스트 스타일
      • 가치 있는 단위 테스트를 위한 리팩토링
      • 통합 테스트
      • Cross Browsing Testing
      • 기능 테스트 종류
      • React Testing Pattern
      • 프론트엔드 테스트 입문
        • 테스트 범위
        • 단위 테스트 검증
        • Mock
        • UI 컴포넌트 테스트
        • 테스트 커버리지
        • 웹 통합 테스트
        • MSW
        • 스토리북
        • 시각적 회귀 테스트
        • E2E 테스트
        • Github Actions 설정
        • 깃허브 액션에서 E2E
      • 시프트 레프트
        • 테스트 기본중의 기본
        • 단위 테스트
        • 코드 복잡도
        • 리팩터링
        • 코드 리뷰
        • 통합 테스트 패턴
        • 시스템 테스트의 자동화
        • 탐색적 테스트
      • Test Tip
      • vitest
      • playwright
      • Test Data Generator
      • MSW
    • Algorithm
      • coding test
      • Data Structure
    • Next.js
      • Data Fetching
      • Hydration
      • Next 13
      • Optimization
      • Next 15
    • Tailwind
      • Tailwind CSS
      • Theme
      • Directives
      • Tool
      • Design System
    • Storybook
      • Storybook
      • CSF3
      • CDD
      • Headless Component
    • Funtional Programming
      • 함수형 프로그래밍
      • 참조 투명성
      • 부수효과
      • 함수 합성
      • 제너릭 타입 활용하기
      • 암묵적 입출력
      • 액션과 계산, 데이터
      • 계층형 설계
      • 호출 그래프
      • 함수형 설계
      • 불변성
      • 일급 함수
      • 함수형 도구
    • Git
      • Github Actions
      • Conflict
      • Branch 전략
    • Contents Format
      • Audio
    • 3D Graphic
      • 3D keyword
      • Three.js
      • Geometry
      • Material
      • Light
      • Camera
      • Decal
      • Rotation
      • Text
      • Shadow
      • Fog
      • Post Processing
      • Animation
      • Math
        • Vector Space
        • 벡터의 연산
        • 회전 계산
      • 3D 컨텐츠가 만들어지는 과정
      • R3F
      • Env
      • Scene
      • Transform
      • R3F
      • Interaction & Raycast
      • Rendering Algorithnm
      • Blender
      • Blender
    • Accessibility
      • 접근성이란
    • Interactive Web
      • Parallax
      • Canvas
      • requestAnimationFrame
      • Effect
      • HSL
      • React.js + Canvas
      • Matter.js
    • AWS
      • DevOps
      • Amplify
      • S3
      • 클라우드 컴퓨팅
        • 온프레미스와 클라우드
        • 클라우드 도입효과
        • 클라우드 컴퓨팅의 범위
        • 컴퓨팅 옵션
          • EC2 - Virtual Machin
          • ECS, EKS - Container
          • Lambda - Serverless
        • 네트워크 가상화
        • 스토리지
        • 데이터베이스
        • 데이터 수집
        • 머신 러닝 영역
        • IoT 영역
        • 블록체인 영역
      • 클라우드 아키텍처 설계
    • Network
      • Web Server & WAS
    • System Design
      • System Design
      • Component
      • 의존성을 배제한 개발
      • Error Handling
      • Architecture
        • 모노로틱 아키텍처
        • Clean Architecture
        • Layered Architecture
        • 이벤트 기반 아키텍처
      • 상황을 파악하는 메타인지
      • 중복 문제 해결하기
      • Monorepo Arhitecture
        • 모노레포 운영과 트러블슈팅
        • Module Federation
      • 코드 병목지점
      • API 대응
      • 공통 코드
      • Infra 구축
      • 모듈 기반의 개발 방식
      • Design System
        • 최소 수준의 아키텍처 설정
        • 더 효율적인 디자인시스템 만들기
        • 디자인 시스템과 UI 라이브러리 목적
        • 디자인 토큰
      • 효율적인 업무
        • 업무 프로세스 병목 파악
      • Clean Code
      • Design Pattern
        • CQRS Pattern
        • Strangler Fig Pattern
        • 데코레이터 패턴
        • 커맨드 패턴
        • 전략 패턴
        • 옵저버 패턴
      • A/B 테스팅
      • 대규모 리엑트 웹앱 개발
        • 복잡성 관리
        • 모듈성
        • 성능
        • 디자인 시스템
        • 데이터 패칭
        • 상태 관리
        • 국제화
        • 코드 조직화하기
        • 개인화 A/B 테스팅
        • 확장 가능한 웹 아키텍처
        • 테스팅
        • 툴링
        • 기술적 마이그레이션
        • 타입스크립트
        • 라우팅
        • 사용자 중심 API 디자인
        • 리액트 미래
    • Performance
      • React DevTools
      • Component 최적화
      • Page Load
      • API
    • MFA
      • MSA
      • MFA 도입하기
      • Monorepo
        • Monorepo Tool
        • Yarn Berry Workspace
        • Turborepo
      • MFA Composition
      • SPA 통합
      • Design System
      • Package Manager
        • Yarn
        • pnpm
      • Transpiler & Bundler
        • Babel
        • Rollup
        • esbuild
        • swc
        • Webpack
        • Vite
      • 분해와 통합을 위한 여러 기술 비교
    • State Management
      • Zustand
    • React v18
      • Automatic batching
      • Suspense
      • Transition
    • SEO
      • Search Engine Optimization
      • Open Graph Element
      • Metadata
    • FE Develop
      • Scrubbing
      • Clipboard
    • Refactoring
      • 리팩토링 깊게 들여다보기
      • 긴 코드 조각내기
      • 타입 코드 처리하기
      • 유사한 코드 융합하기
      • 데이터 보호
      • 코드 추가 및 제거
    • OAuth 2.0
    • Analytics
      • Mixpanel
    • ETC
      • VSCode
    • React Hook In Action
      • useContext & Provider
      • 커스텀 훅
      • 코드 분할하기 with Suspense, lazy
      • Suspense와 이미지 적재하기
      • useTransition, uesDeferredValue
      • SuspenseList
Powered by GitBook
On this page
  • 스토리북
  • 설치
  • 스토리
  • 에드온
  • Context API에 의존하는 스토리 등록
  • 웹 API에 의존하는 스토리 등록
  • Next.js Router에 의존하는 스토리
  • Play function을 활용한 인터렉션 테스트
  • addon-a11y를 활용한 접근성 테스트
  • 스토리북 테스트 러너
  • 스토리를 통합 테스트에 재사용하기
  1. DEV_NOTE
  2. Testing
  3. 프론트엔드 테스트 입문

스토리북

스토리북

  • 디자이너나 프로젝트 리더와 구현된 UI 컴포넌트의 공유가 가능해지면 협업시 능률이 높아진다.

  • UI 컴포넌트 탐색기는 구현된 UI 컴포넌트를 쉽게 공유할 수 있도록 도와주는 협업 도구

  • 스토리북의 UI 컴포넌트 테스트는 통합 테스트, E2E 테스트 중간에 위치한 테스트

    • 스토리북은 UI 컴포넌트 탐색기지만 테스트 기능도 강화했음

설치

npx storybook init
> webpack 5 //or vite 번들러 설정
> y //샘플 코드 설치할지
> react // 어떤 라이브러리의 샘플 코드를 선택할지
> npm run storybook

스토리

  • 스토리 최신 포맷 CSF3.0

  • CSF3.0은 객체를 개별적으로 export 하여 스토리 등록함

import { Button } from './Button';

export default {
  title: "Example/Button",
  component: Button,
}
// 다른 이름으로 export 하게 되면 다른 스토리로 등록됨
export const Default = {
  args: {
    label: "Button",
  },
}
  • 모든 스토리에는 Global, Component, Story 라는 세 단계의 설정이 깊은 병합 방식으로 적용됨

    • Global < Component < Story 순으로 설정의 우선순위가 높아진다.

  • 공통으로 적용할 항목을 적절한 스코프에 설정하여 스토리마다 개별적으로 설정해야하는 작업을 최소화

  • Global 단계: 모든 스토리에 적용할 설정 (.storybook/preview.ts)

  • Component 단계: 스토리 파일에 적용할 설정 (export default)

  • Story 단계: 개별 스토리에 적용할 설정 (export const)

에드온

  • 에드온으로 필요한 기능을 추가할 수 있음

  • 스토리북을 설치할 때 기본적으로 추가되는 @storybook/addon-essentials 은 필수 에드온

Controls를 활용한 디버깅

  • Props에 전달된 값에 따라 컴포넌트는 다른 스타일과 기능을 제공한다.

  • 스토리북은 Props를 변경해 컴포넌트가 어떻게 표시되는지 실시간으로 디버깅 가능

    • 이를 Controls라고 한다.

  • @Storybook/addon-controls 라는 에드온,

    • @storybook/addon-essentials 에 포함되어있음

Actions를 활용한 이벤트 핸들러 검증

  • 이벤트 핸들러가 어떻게 호출됐는지 로그를 출력하는 기능

  • @storybook/addon-actions

    • 마찬가지로 @storybook/addon-essentials 에 포함되어있음

  • Global 단계 설정인 .storybook/preview.js 를 보면 argTypesRegex: "^on[A-Z].*" 라는 설정이 on 으로 시작하는 모든 이벤트 핸들러는 자동적으로 Actions 패널에 로그를 출력하게 된다.

    • ⚠️ 만약 프로젝트에 이벤트 핸들러의 이름으로 사용하는 다른 네이밍 컨벤션이 있다면 해당 컨벤션에 따라 정규표현식을 수정해야 한다.

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
}

반응형 대응을 위한 뷰포트 설정

  • 반응형으로 구현된 UI 컴포넌트는 화면 크기별로 스토리를 등록할 수 있다.

  • @storybook/addon-viewport 에서 지원

  • SP(스마트폰) 레이아웃으로 스토리를 등록하려면 parameters.viewport 를 설정해야 한다.

import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';

export const SPStory = {
  parameters: {
    viewport: {
      viewports: INITIAL_VIEWPORTS,
      defaultViewport: "iphone6"
    },
    screenshot: {
      viewport: {
        width: 375,
        height: 667,
        deviceScaleFactor: 1,
      },
      fullPage: false,
    }
  }
}
// SPStory: SP 레이아웃용 커스텀 공통 설정 모듈
import { SPStory } from '@/tests/storybook';

export const SPLoggedIn: Story = {
  parameters: {
    ...SPStory.parameters,
  }
}

Context API에 의존하는 스토리 등록

  • 리액트의 Context API 에 의존하는 스토리에는 스토리북의 데코레이터(decorator) 를 활용

  • 초깃값을 주입할 수 있도록 Provider를 만들면 Context 의 상태에 의존하는 UI를 간단하게 재현 가능

스토리북의 데코레이터

  • 데코레이터란 스토리의 렌더링 함수에 적용할 래퍼(wrapper)

import { ChildComponent } from "./";

export default { // 컴포넌트 단계의 설정
  title: "ChildComponent",
  component: ChildComponent,
  decorators: [
    (Story) => (
      <div style={{ padding: "60px" }}> // 해당 파일의 스토리들이 모두 적용됨
        <Story /> // 각 스토리가 전개됨
      </div>
    )
  ]
}
  • 로그인한 사용자 정보가 있는 Provider(LoginUserInfoProvider) 를 데코레이터가 소유했다면 Context의 Provider에 의존하는 UI 컴포넌ㅌ의 스토리에서도 로그인한 사용자의 정보를 표시할 수 있다.

import { LoginUserInfoProvider } from "@/components/providers/LoginUserInfo"
import { Args, PartialStoryFn } from '@storybook/csf';
import { ReactFramework } from '@storybook/react';

export const LoginUserInfoProviderDecorator = (
  Story: PartialStoryFn<ReactFramework, Args>
) => (
  <LoginUserInfoProvider>
    <Story /> // 스토리가 Context를 통해 LoginUserInfo를 참조
  </LoginUserInfoProvider>
)
  • 유사하게 공통 레이아웃을 제공할 데코레이터를 만들면 상황에 따라 사용할 수 있다.

  • 앱에서 필요한 Provider 라면 실제 구현 코드와 똑같이 사용해도 상관없지만, 이 밖에는 스토리북 전용 Provider를 데코레이터로 만드는것이 좋다.

import { BasicLayout } from "@/components/layouts/BasicLayout"
import { Args, PartialStoryFn } from '@storybook/csf';
import { ReactFramework } from '@storybook/react';

export const BasicLayoutDecorator = (
  Story: PartialStoryFn<ReactFramework, Args>
) => BasicLayout(<Story />)

데코레이터 고차함수

  • 데코레이터를 만드는 함수(고차 함수)를 작성하면 쉽게 데코레이터 작성 가능

  • example

    • Provider Context 는 { message, style } 라는 상태를 소유하는데, 이 상태를 통해 정보를 통지

    • 각 스토리는 createDecorator 라는 고차 함수를 사용해 설정을 최소화

// 사용 예시
export const Succeed: Story = {
  decorators: [createDecorator({ message: "성공했습니다.", style: "succeed" })],
};

export const Failed: Story = {
  decorators: [createDecorator({ message: "실패했습니다.", style: "failled" })],
}

export const Busy: Story = {
  decorators: [createDecorator({ message: "통신 중.", style: "busy" })],
}
  • <ToastProvider> 가 제공하는 정보를 <Toast> 컴포너트가 표시

  • 이와 같이 고차함수를 만들면 초기값(defaultState)를 쉽게 주입 가능

import { ToastProvider, ToastState } from '@/components/providers/ToastProvider';
import { ComponentMeta, ComponentStoryObj } from '@storybook/react';
import { Toast } from '.';

function createDecorator(defaultState?: Partial<ToastState>) {
  return function Decorator() {
    return (
      <ToastProvider defaultState={{ ...defaultState, isShown: true }}>
        {null}
      </ToastProvider>
    )
  }
}

웹 API에 의존하는 스토리 등록

  • 웹 API에 의존하는 컴포넌트는 스토리에도 웹 API가 필요함

    • 컴포넌트를 렌더링하려면 웹 API 서버를 실행하고 있어야 한다.

  • 스토리북을 빌드해서 정적 사이트로 호스팅할 대도 서버 상태가 좋지 않다면 API 요청이 원활히 이뤄지지 않게됨

  • 이와 같은 UI 컴포넌트는 MSW를 사용해야 한다.

에드온 설정

npm i msw msw-storybook-addon --save-dev
  • 스토리북에서 MSW를 사용하려면 msw와 msw-storybook-addon 을 설치해야 한다.

  • .storybook/preview.js 에서 initialize 함수를 실행해 MSW를 활성화

    • mswDecorator 는 모든 스토리에 필요하므로 글로벌 단계에서 설정

// .storybook/preview.js
import { initialize, mswDecorator } from 'msw-storybook-addon';

export const decorators = [mswDecorator];

initialize();
  • 프로젝트에 처음 MSW를 설치한다면 public/ 경로를 다음의 커맨드로 실행

    • (<PUBLIC_DIR> 을 프로젝트의 public 디렉토리 명으로 변경)

    • 커맨드를 실행하면 mockServiceWorkjer.js 가 생성되며 커밋해야함

npx msw init <PUBLIC_DIR>
  • 스토리북에도 public/ 경로를 명시

// .storybook/main.js
module.exports = {
  //...
  staticdirs: ["../public"],
}

요청 핸들러 변경

  • 다른 parameters 와 동일하게 Global, Component, Story 설정을 통해 스토리에 사용할 요청 핸들러가 결정됨

  • .storybook/preview.js 에 필요한 설정을추가

    • 예를 들어 모든 스토리에 로그인한 사용자 정보가 필요하다면 로그인한 사용자 정보를 반환하는 MSW 핸들러를 Global 단계에 설정하는 것이 좋다.

export const parameters = {
  // 기타 설정 생략
  msw: {
    handlers: [
      rest.get("/api/my/profile", async (_, res, ctx) => {
        return res(
          ctx.status(200),
          ctx.json({
            id: 1,
            name: "EonsuBae",
            bio: "..."
            likeCount: 1,
            //...
          })
        )
      }
    ]
  }
}
  • 요청 핸들러가 스토리에 적용되는 우선순위는 Story > Component > Global

  • 요청 핸들러는 스토리마다 독립적으로 설정할 수 있기 때문에 같은 컴포넌트에서 웹 API 응답에 따라 다른 내용 표시 쌉가능

  • 오류 응답 상태 코드에 따라 다른 내용이 표시되는 상황도 검증 가능

고차 함수로 요청 핸들러 리팩터링

  • 웹 API에서 URL과 응답 내용은 불가분의 관계

  • 스토리나 테스트에 URL을 하드코딩하면 URL 사양이 변경돼도 변경된 사양이 반영되지 않게됨

  • 이를 피하려면 웹 API 클라이언트 설정에 있는 고차 함수를 통해 만든 요청 핸들러에 URL을 정의해야함

    • handleGetMyProfile 는 따로 정의한 msw 요청 핸들러

  • 클라이언트 설정 부분에서 요청 핸들러와, 고차 함수(데코레이터)를 제데로 활용하면 코드가 간결해짐

import { rest } from "msw";
import { path } from "..";
import { getMyProfileData } from "./fixture";

export function handleGetMyProfile(args?: {
  mock?: jest.Mock<any, any>;
  status?: number;
}) {
  return rest.get(path(), async (_, res, ctx) => {
    args?.mock?.();
    if (args?.status) {
      return res(ctx.status(args.status));
    }
    return res(ctx.status(200), ctx.json(getMyProfileData));
  });
}

export const handlers = [handleGetMyProfile()];
export const NotLoggedIn: Story = {
  parameters: {
    msw: { handlers: [handleGetMyProfile({ status: 401 })] }
  }
}
// 로그인 화면의 스토리 파일
export default {
  component: Login,
  parameters: {
    nextRouter: { pathname: "/login" },
    msw: { handlers: [handleGetMyProfile({ status: 401 })] },
  },
  decorators: [BasicLayoutDecorator],
} as ComponentMeta<typeof Login>;

Next.js Router에 의존하는 스토리

  • 컴포넌트 중 특정 URL에서만 사용할 수 있는 컴포넌트가 존재함

  • 이와같은 컴포넌트의 스토리를 등록하려면 에드온이 필요

    • storybook-addon-next-router

    • 에드온을 추가하면 Router 상태를 스토리마다 설정 가능해짐

에드온 설정

.storybook/main.js 와 .storybook/preview.js 에 설정을 추가

// .storybook/main.js
module.exports = {
  //...
  stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: ["storybook-addon-next-router"]
}
// .storybook/preview.js
import { RouterContext } from "@next/dist/shared/lib/router-context";

export const parameters = {
  //...
  nextRouter: {
    Provider: RouterContext.Provider
  }
}

Router에 의존하는 스토리 등록

  • 헤더의 네비게이션에는 pathname(브라우저 URL) 에 따라 메뉴 하단의 스타일이 달라진다고 해보자

export const RouteMyPosts: Story = {
  parameters: {
    nextRouter: { pathname: "/my/posts" },
  }
}

export const RouterMyPostsCreate: Story = {
  parameters: {
    nextRouter: { pathname: "my/posts/create" },
  }
}

Play function을 활용한 인터렉션 테스트

  • props 를 전달해 다양한 상황을 재현 가능하지만 UI에 인터렉션을 할당하는 방법으로도 재현하는 상황도 존재

    • 폼에서 값을 전송하기 전 브라우저에 입력된 내용에 유효성 검사를 실시해 문제가 있으면 오류를 표시하는 상황

    • 문자 입력, 포커스 아웃 이벤트, 전송 버튼 클릭 등 과 같은 인터렉션이 필요함

    • 스토리북 기능인 play function 를 사용하면 인터렉션 할당 상태를 스토리로 등록 가능

에드온 설정

$ npm i @storybook/testing-library @storybook/jest @storybook/addon-interactions --save-dev
  • @sotrybook/testing-library , @storybook/jest, @storybook/addon-interactions

// .storybook/main.js
module.exports = {
  // 다른 설정은 생략
  stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: ["@storybook/addon-interactions"],
  features: {
    interactionsDebugger: true,
  }
}

인터렉션 할당

  • 인터렉션을 할당하기 위해 스토리에 play 함수를 설정

  • 테스팅 라이브러리 및 jsdom 를 사용할 때와 동일하게 userEvent 를 사용해서 컴포넌트에 인터렉션 할당

  • 테스팅 라이브러리에서 사용하는 getBy , userEvent 거의 동일한 API라 테스트를 작성하는 느낌으로 작성

  • 스토리가 렌더링될 때 play function 이 자동으로 실행돼 문자가 입력된 것을 확인 가능

    • 인터렉션이 제데로 할당하지 않으면 도중에 인터렉션이 중단됨

export const SucceedSaveAsDraft: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await user.type(
      canvas.getByRole("textbox", { name: "제목" }),
      "나의 기사"
    )
  }
}

단언문 작성 (테스트 검증)

  • @storybook/jest 의 expect 함수를 사용하면 컴포넌트에 인터렉션을 할당한 상태에서 단언문 작성가능

export const SavePublish: Story = {
  play: async ({ canvasElement }) => {
    const canavs = within(canvasElement);
    await user.type(
      canvas.getByRole("textbox", { name: "제목" }),
      "나의 기사"
    );
    await user.click(canvas.getByRole("switch", { name: "공개 여부" }));
    await expect(
      canvas.getByRole("button", { name: "공개하기" })
    ).toBeInTheDocument()
  }
}
// 아무것도 입력하지 않고 비공개 상태로 저장을 시도하면 유효성 검사가 실패해 오류가 표시됨
export const FailedSaveAsDraft: Story = {
  play: async ({ canvasElement }) => {
    const canavs = within(canvasElement);
    await user.click(canvas.getByRole("switch", { name: "비공개 상태로 저장" }));
    const textbox = canvas.getByRole("textbox", { name: "제목" });
    await waitFor(() => 
      expect(textbox).toHaveErrorMessage("한 글자 이상의 문자를 입력하세요")
    );
  }
}

addon-a11y를 활용한 접근성 테스트

  • 스토리북은 컴포넌트 단윙로 접근성을 검증하는 용도로도 활용됨

  • 스토리북을 확인하면서 코드를 작성하면 접근성에 문제가 있는지 조기에 발견 가능

에드온 설정

  • @storybook/addon-a11y 에드온을 추가하면 스토리북 탐색기에서 접근성 관련 우려사항을 알려줌

  • parameters.a11y에 해당 에드온 설정을 사용

    • 다른 parameters 와 동일하게 Global > Compeonnt > Story 단계로 나눠 적용 가능

// .storybook/main.js
module.exports = {
  // 다른 설정 생략
  stories: ["../src/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: ["@storybook/addon-a11y"],
}

접근성과 관련한 주의 사항 점검

  • 에드온 패널에 추가된 Accessibility 패널을 열어보면 검증한 내용이 Violations, Passes, Incomplete로 구분됌

    • Violations: 접근성을 위반했다

    • Incomplete:수정이 필요하다

  • Highlight results 를 체크하면 주의점이 점선으로 표시되며 강조됨

일부 규칙 위반을 무효화하기

  • 규칙이 지나치게 엄격하다고 느끼거나 원하지 않는 보고 내용이 있는 경우 일부 내용을 무효화 할 수 있다.

  • 무효화 기능도 마찬가지로 Global, Compoennt, Story 나눠 설정 가능

    • 해당 에드온은 접근성 검증도구로 axe를 사용한다.

    • parameters 등의 세부 설정은 axe-core의 공식문서를 참고하자.

export default {
  component: Switch,
  parameters: {
    a11y: {
      config: { rules: [{. id: "label", enabled: false }] }
    }
  }
} as ComponentMeta<typeof Switch>;

접근성 검증 생략

  • 접근성 검증 자체를 생략하고 싶다면 parameters.a11y.disable 를 true 로 설정

  • 일부 규칙 위반 무효화와 달리 접근성 자체를 검증 대상에서 제외시키므로 신중히 사용

export default {
  component: Switch,
  parameters: {
    a11y: { disable: true }
  }
} as ComponentMeta<typeof Switch>;

스토리북 테스트 러너

  • 스토리북의 테스트 러너는 스토리를 실행가능한 테스트로 변환한다.

  • 테스트로 변환된 스토리는 제스트와 플레이라이트에서 실행된다.

  • 이 기능을 활용해서 스토리북에 스모크 테스트는 물론 play function 을 활용한 인터렉션 테스트, 접근성 테스트 등을 할 수 있어 UI 컴포넌트 테스트로도 활용 가능

    • 스모크 테스트: 제품의 기본적인 작동 여부를 대략적으로 검증하는 테스트를 말함

테스트 러너를 활용한 테스트 자동화

  • 컴포넌트가 변경되면 등록된 스토리에도 변경 사항을 반영해야 한다.

    • 예를 들면 props 가 변경되거나 의존하는 웹 API의 데이터가 변경됐다면 스토리에도 변경사항을 반영해야 오류가 발생하지 않게됨

  • @storybook/test-runner 를 사용해서 명령줄 인터페이스나 CI에서 테스트 러너를 실행하면 등록된 스토리에 오류가 없는지 검증할 수 있다.

// package.json
{
  "scripts": {
    "test:storybook": "test-storybook"
  }
}

테스트 러너를 활용한 play function 테스트 자동화

  • play function을 사용하는 스토리에 컴포넌트의 변경사항을 반영하지 않으면 도중에 인터렉션이 실패됨

  • 테스트 러너는 play function 을 사용하는 스토리를 대상으로 인터렉션 오류 없이 정상적으로 종료됐는지 검증한다.

  • 복잡한 인터렉션이 있는 경우에도 육안으로 확인하면서 테스트할 수 있기 떄문에 테스팅 라이브러리와 jest-dom 을 사용할 때보다 편하게 테스트 코드를 작성할 수 있다. (스토리북의 장점)

// 모바일 한정으로 드로어 메뉴를 렌더링하고, 스토리에는 버튼을 클릭해서 드로어 메뉴를 여는 인터렉션이 등록됨
export const SPLoggedInOpenedMenu: Story = {
  storyName: "Sp 레이아웃에서 드로어 메뉴를 연다",
  parameters: {
    ...SPStory.parameters,
    screenshot: {
      ...SPStory.parameters.screenshot,
      delay: 200,
    }
  },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const button = await canvas.findByRole("button", {
      name: "메뉴 열기",
    });
    await user.click(button);
    const navigation = canvas.getByRole("navigation", {
      name: "내비게이션"
    });
    await expect(navigation).toBeInTheDocument();
  }
}

뷰포트 설정이 반영되지 않는 문제 (이미 해결됨)

  • 이슈를 해결하기 위한 임시방편 코드

module.exports = {
  async preRender(page, context) {
    if (context.name.startsWith("SP")) {
    // SP로 시작하는 스토리의 뷰포트를 스마트폰 사이즈로 고정
      page.setViewportSize({ width: 375, height: 667 }); 
    } else { // 그외 모든 스토리의 뷰포트를 PC 사이즈로 고정
      page.setviewportSize({ width: 1280, height: 800 });
    }
  }
}

테스트 러너를 활용한 접근성 테스트 자동화

  • 스토리북의 테스트 러너는 플레이라이트와 헤드리스 브라우저에서 실행된다.

    • 덕분에 플레이라이트의 생태계에 있는 기능들을 테스트 러너에서 활용할 수 있다.

    • axe-playwright 는 접근성 검증 도구인 axe를 사용하는 라이브러리로서 접근성 관련 문제점을 찾음

$ npm i axe-playwright --save-dev
  • 기본 설정을 사용하면 Incomplete도 오류로 검출된다.

  • 만약 오류가 너무 많다면 includedImpacts 에 critical 만 설정해서 Violations 에 해당하는 오류만 검출되도록 변경 가능

  • 오류 검출 수준을 조정하면서 단계적으로 개선을 시도할 수 있다.

// .storybook/test-runner.js
const { getStoryContext } = require("@storybook/test-runner");
const { injectAxe, checkA11y, configureAxe } = require("axe-playwright");

module.exports = {
  async preRender(page, context) {
   if (context.name.startsWith("SP")) {
      page.setViewportSize({ width: 375, height: 667 }); 
    } else { 
      page.setviewportSize({ width: 1280, height: 800 });
    }
    await injectAxe(page); // axe를 사용하는 검증 설정
  },
  async postRender(page, context) {
    const storyContext = await getStoryContext(page, context);
    if (storyContext.parameters?.a11y?.disable) {
      return;
    }
    await configureAxe(page, {
      rules: storyContext.parameters?.a11y?.config?.rules,
    });
    await checkA11y(page, '#root', { // axe를 사용한 검증
      includedImpacts: ["critical", "serious"], // Vioations에 해당하는 오류만 검출
      detailedReport: false,
      detailedReportOptions: { html: true },
      axeOptions: storyContext.parameters?.a11y?.options,
    })
  }
}

스토리를 통합 테스트에 재사용하기

  • 제스트로 작성한 테스트 코드와 더불어 스토리까지 커밋해야 한다면 운용비가 너무 많이 증가하는것 아닌가 염려됨

    • 스토리를 통합 테스트에 재사용해서 해결할 수 있다.

    • 양쪽을 모두 커밋하면서도 운용비가 줄어든다.

스토리 재사용

  • 컴포넌트 테스트는 검증을 시작하기 전에 상태를 만들어야 한다.

  • 상태를 만드는 일과 스토리를 만드는 일은 거의 동일한 작업

// 스토리 등록용 함수
function createDecorator(defaultState?: Partial<AlertDialogState>) {
  return function Decorator(Story: PartialStroyFn<ReactFramework, Args>) {
    return (
      <AlertDialogProvider defaultState={{ ...defaultState, isShown: true }}>
        <Story />
      </AlertDialogProvider>
    )
  }
}

// 실제로 등록할 스토리
export const Default: Story = {
  decorators: {createDecorator({ message: "성공했습니다." })],
}

export const CustomButtonLabel: Story = {
  decorators: {
    createDecorator({ 
      message: "기사를 공개합니다. 진행하시겠습니까?" 
      cancleButtonLabel: "CANCEL",
      okButtonLabel: "OK",
    })
  ],
}

export const ExcludeCancel: Story = {
  decorators: {
    createDecorator({ 
      message: "전송됐습니다" 
      cancleButtonLabel: undefined,
      okButtonLabel: "OK",
    })
  ],
}
  • 해당 컴포넌트는 AlertDialogProvider 에 의존하지만 createDecorator 를 사용하면 초기값을 주입할 수 있어 다양한 스토리를 등록할 수 있다.

  • Context API에 의존하는 컴포넌트, 즉 AlertDialogProvider가 없으면 작동하지 않는다.

    • 이는 테스트할 때도 마찬가지

    • 테스트의 render 에 매번 AlertDialogProvider 를 추가해야 한다.

    • 매번 이를 추가하는 것이 언급한 상태를 미리 만들어 두는 작업에 해당한다.

  • 스토리를 등록할 때 이미 createDecorator 함수로 상태를 만들었지만 테스트 코드에도 같은 작업을 반복한다면 중복 작업이라고 느낄 수 있다.

  • 스토리를 테스트 대상으로 만들어 재활용하면 컴포넌트 테스트를 위한 상태를 재차 정의하지 않아도 된다.

    • 이것이 스토리의 재사용 방법

스토리를 import하여 테스트 대상으로 만들기

  • 테스트에 스토리를 재사용하기 위해 import 하려면 전용 라이브러리인 @storybook/testing-react 를 사용해야 한다.

  • 스토리 파일의 내용을 불러온 후 composeStories(stories) 로 각 스토리를 선언하면 테스트 준비 완료

  • 단언문은 스토리를 render 한 후 작성할 수 있기 때문에 스토리를 테스트의 일부라고 생각하자.

import { composeStories } from "@storybook/tresting-react";
import { render, screen } from "@testinb-library/react";
import * as stories from "./index.stories";  // jest를 사용하는 테스트에 스토리 파일 불러오기

const { Default, CustomButtonLabel, ExcludeCancel } = composeStories(stories);

describe("AlertDialog", () => {
  test("Default", () => {
    render(<Default />); // 스토리 렌더링
    expect(screen.getByRole("alertDialog")).toBeInTheDocument();
  })
  
  test("CustomButtonLabel", () => {
    render(<CustomButtonLabel />); // 스토리 렌더링
    expect(screen.getByRole("button", { name: "OK" })).toBeInTheDocument();
    expect(screen.getByRole("button", { name: "CANCEL" })).toBeInTheDocument();
  })
  
  test("ExcludeCancel", () => {
    render(<ExcludeCancel />); // 스토리 렌더링
    expect(screen.getByRole("button", { name: "OK" })).toBeInTheDocument();
    expect(screenqueryByRole("button", { name: "CANCEL" })).not.toBeInTheDocument()
  })
})

@storybook/test-runner와의 차이점

  • 테스트와 스토리 등록을 한번에 처리해서 중복을 줄이는 방법은 테스트 러너 활용법(스토리의 play function에 단언문 작성하기)과 유사하다.

  • 어떤 방법을 적용할지는 테스트 목적과 각 방법의 장단점을 비교해 결정하자.

  • Jest에서 스토리를 재사용할 때의 장점

    • 목 모듈 혹은 스파이가 필요한 테스트를 작성할 수 있다.(제스트의 목함수 사용)

    • 실행 속도가 빠르다. (헤드리스 브라우저를 사용하지 않기 때문)

  • 테스트 러너의 장점

    • 테스트 파일을 따로 만들지 않아도 된다.(적은 작업량)

    • 실제 환경과 유사성이 높다. (브라우저를 사용해 CSS가 적용된 상황 재현 가능)

PreviousMSWNext시각적 회귀 테스트

Last updated 9 months ago

rules 에 있는 id(여기선 "label")은 axe-core의 을 참고해 적용

가 있을 수 있음

Rule Descriptions
스토리마다 설정한 뷰포트가 테스트 러너에 적용되지 않는 이슈