StoryBook으로 컴포넌트 테스트

2023년 4월 18일

FrontEnd Test

테스트는 학창시절 까지만 해도 단순 시험이였다. 지금도 그렇다. 테스트는 시험이다. 무엇을 위한 시험인가?

개발에서 테스트란 내가 만든 코드가 잘 구현하는지, 오류나 버그가 없는지 등을 파악하기 위해서 테스트 코드를 작성하고 시험해본다. 실제로 사용자에게 서비스가 제공되기 전 거쳐야 하는 과정이다.

사실 이미 테스트를 하면서 개발을 하고 있었다...

console.log()

바로 console.log()를 통해 테스트를 하고 있었다. "이게 무슨 테스트야" 라고 생각할 수 있지만, 나와 같은 주니어 개발자들은 어떤 기능을 구현하고, 내가 원하는 값이 잘 출력 되는지 알기 위해서 항상 콘솔로그를 출력해 확인하고 있고, 로직이 조금 복잡하다면, 중간 중간에 로그를 출력하면서, 어디서 값이 잘못 되는지 판단하고 있었다.

그렇다고 다른 테스트 방법을 알지 않고, console.log()로 코드를 도배하는 것도 문제가 된다.

이제 테스트를 왜 해야하고, 어떤 방법이 있는지 등 알아보겠다.

테스트 종류

  • End-to-End
    • 사용자 입장에서 애플리케이션이 잘 동작하는 지 전체적으로 테스트
  • Integration (통합)
    • 실제 DB, 브라우저 없이 큰 규모의 기능이나 하나의 페이지가 잘 작동하는 지 테스트
  • Unit (단위)
    • 기능의 개별적인 단위나 하나의 컴포넌트를 테스트
  • Static (정적)
    • 구문 오류, 나쁜 코드 스타일, 잘못된 API 사용 등을 린팅

테스트를 하는 이유

  1. 기능적 정확성

    • 애플리케이션이 예상대로 작동하고 기능적 요구 사항을 총족하는지 확인합니다.
    • 사용자 경험에 부정적인 영향을 미칠 수 있는 버그나 오류 및 예기치 않은 동작을 포착하는 데 도움이 됩니다.
  2. 사용자 인터페이스 유효성 검사

    • 레이아웃과 스타일 및 상호 작용과 같은 인터페이스 요소의 유효성 검사를 할 수 있습니다.
    • 이러한 UI의 테스트는 Storybook과 같은 도구를 활용할 수 있습니다.
    • 흔히 반응형 웹을 만들고, 브라우저와 다양한 장치(폰,테블릿,PC,TV 등)에 맞게 UI가 구현 됐는지 판단하기 위함입니다.
  3. 성능 및 최적화

    • 애플리케이션이 응답이 빠르고 로드되며 잘 수행되는지 판단하기 위해서 입니다.
    • 사용자 경험을 저하시킬 수 있는 잠재적인 병목 현상 파악
    • 비효율적인 코드 또는 리소스를 많이 사용하는 작업을 식벽하는 데 도움이 됩니다.
    • 테스트를 통해 시간도 측정할 수 있어, 어떤 로직의 연산 시간이 길다면, 알고리즘 측면에서 문제를 해결할 수 있고, 로직을 좀 더 단순화 시켜 성능을 높일 수 있습니다.
  4. 백엔드 서버와 통합

    • API간의 통합 테스트를 통해, 데이터 통신, 인증 등 전반적인 상호 작용 검증하는데 도움이 됩니다.
    • mocking을 통해 가짜 서버를 만들어 API통신을 테스트할 때 발생하는 문제를 해결할 수 있습니다.
  5. CI/CD

    • 지속적인 배포, 지속적인 통합을 통해 프로덕션에 배포하기 전에 변경 사항을 검증하기 위해 자동화된 테스트를 할 수 있습니다.
    • 코드의 품질을 유지하고 잘못된 코드 배포를 방지하며 안정적인 애플리케이션을 보장하는데 도움이 됩니다.

고려해야하는 것들

  1. 프로젝트 규모와 요구 사항 +@일정
    • 모든 프로젝트의 목적은 완벽이 아니라 완성이기 때문에, 문제가 없고 좋은 코드도 중요하지만, 우선 순위를 정하고 테스트를 진행하는게 좋습니다.
  2. 애플리케이션의 전체적인구성
    • 모든 곳을 테스트 하기 보단, 필요한 부분에 테스트를 진행하는 것이 좋습니다.
  3. 테스트 도구 설정 난이도
    • 프로젝트를 진행하는 개발자들이 개발 이외 테스트를 진행함에 따라 문제 없이 진행할 수 있는 어렵지 않고, 충분히 할 수 있는 수준의 테스트 도구로 진행하는게 좋습니다.
  4. 테스트 도구 지향점
    • jest, cypress, storybook 등의 테스트 도구들을 목적에 맞게 선택해서 사용해야 합니다.
  5. 선행 지식
    • 개발 진행하기도 어려운데, 테스트 이론, 도구 등 학습을 같이 하면서 개발을 진행하면, 많은 시간이 걸리고, 개발자들도 많은 어려움이 생기니, 프로젝트 시작전 진행할 프로젝트의 테스트를 미리 학습하는 것도 중요한 것 같습니다.

TDD

TDD (Test Driven Development - 테스트 주도 개발)이란 무엇인가?

많은 개발론 중 하나로 테스트를 기반으로 테스트가 개발을 이끌어 나가는 형태의 개발론입니다.

쉽게 설명하면, 구현보다 테스트를 먼저 작성하고, 테스트가 통과하면 구현을 하는 방식입니다.

크게 3단계로 실패, 성공, 리팩토링 단계로 이루어져 있습니다.

1. 실패 먼저 실패하는 테스트를 작성합니다. 구현할 기능들을 먼저 테스트로 작성하여, 구현할 기능들을 하나씩 작성하면 됩니다.

2. 성공 두 번째로 성공입니다. 첫 번째 작성했던 실패 테스트를 이제 성공하는 테스트로 만드는 것입니다.

3. 리팩토링 마지막으로 리팩토링 단계 입니다. 구현한 코드에 중복이 있거나, 더 개선할 방법이 있다면 리팩토링을 진행하고, 테스트에 통과하는지 확인합니다. 그리고 다시 첫 번째 단계로 돌아가 계속 진행하면 됩니다.

TDD 장단점

  • 장점

    1. 코드의 품질을 높이고, 버그를 줄이고 유지관리를 더 쉽게 할 수 있도록 도와줍니다.
    2. 실패한 테스트의 문제를 정확히 찾아내므로 더 빨리 수정할 수 있어 디버깅 속도가 빠릅니다.
    3. 코드를 구성하는 방법에 대해 생각하게 되고, 보다 모듈화되고 유연한 설계를 통해 전체적인 코드를 개선할 수 있습니다.
    4. 테스트를 통해 명확한 요구 사항과 문서를 제공함으로써 팀원 구성간의 의사소통이 쉬워 협업에 많은 도움이 됩니다.
  • 단점

    1. TDD의 프로세스를 이해하는데 시간이 걸릴 수 있어, 기능 구현 이외 학습곡선이 필요합니다.
    2. 기능을 바로 구현하지 않고 테스트 부터 작성하기 때문에 개발 속도가 느려질 수 있습니다.
    3. 모든 기능이 완벽할 수 없어, 테스트의 우선순위를 정하지 않으면 2번과 마찬가지로 많은 시간이 걸릴 수 있습니다.
    4. 개선할 기능이 있다면 테스트 역시 개선하기 때문에 추가 노력이 필요합니다.

Test 도구들

  • Jest
    • 거의 모든 기능과 플랫폼을 지원하는 JavaScript Testing Framework
  • React Testing Library
    • BDD 방법론에 어울리며 간결하면서도 꼭 필요한 API 지원
    • Jest와 jsdom기반의 브라우저 DOM Testing
  • Enzyme
    • React Virtual DOM Testing
    • 더이상 사용하지 않고 업데이트도 하지 않음
  • Storybook + Chromatic
    • 컴포넌트 주도의 독립적인 개발 환경 제공
    • 스냅샷 테스트 지원
    • 테스트에 용이한 Addon 지원
  • Cypress
  • Jasmine
  • Karma
  • Selenium
  • Puppeteer

Storybook 이란?

Storybook은 UI 테스트를 할 수 있는 도구로 공식문서에 가면 튜토리얼도 있으니 처음은 어렵겠지만 그래도 공부할 수 있다.

Storybook을 사용하는 이유는 컴포넌트을 시각적으로 정리하고, 컴포넌트마다 문서화를 할 수 있어, 다른 개발자들과 공유할 때도 편리합니다. 또한 개발자 뿐만 아니라, 디자이너, 기획자 등 비개발 직군의 동료들도 쉽게 UI를 테스트할 수 있어 특히 디자이너와 협업에 좋습니다.

또한 프론트엔드에서 개발을 할 때 아마 대부분 top-down 방식으로 개발을 할 것입니다.

프론트엔드에서 top-down 방식이란? 위에서 아래로 즉, 페이지 전체에서 작은 요소로 가는 방식을 말한다. 어떠한 페이지를 구현할 때, 페이지 전체를 구현하고, 그안에 헤더, 메인, 푸터 등을 구현하고, 그안에 메뉴 등 큰 단위에서 작은 단위로 구현하는 방식을 말합니다.

CDD(Component Driven Development)

하지만 스토리북을 사용하면 CDD(Component Driven Development) 방식으로 페이지 단위로 UI개발을 하는게 아닌 컴포넌트 부터 만들고 이를 기반으로 페이지를 구성하는 bottom-up방식으로 구현할 때 효과적입니다.

컴포넌트 주도개발.. 리액트를 사용하는 장점중 하나입니다.

리액트는 UI를 컴포넌트 단으로 관리를 하고 이를 블럭처럼 쌓는 방식으로 UI를 구현합니다.

저도 흔히 리액트로 개발을 할 떄 top-down방식으로 했었기 때문에, CDD를 처음 했을 때 많이 어려웠습니다.

내가 구현할 페이지에 사용할 컴포넌트들을 최대한 재사용할 수 있게, 사용할 속성들을 정의하고 어떻게 해야 현재 프로젝트에서 계속 사용할 컴포넌트를 구현하지? 이러한 생각들이 정말 어려웠습니다. 지금도 쉽지는 않아요.

이러한 방식이 어떠한 방향에서는 비효율적일 수 있습니다. TDD 처럼 개발자가 빨리 기능을 구현하기도 바쁜데, 일일이다 컴포넌트화 시키고, 재사용성 까지 고려한다는게 쉽지 않기 때문이죠.

Storybook 예시

alt

스토리북 배포 주소

위 사진을 보면 오른쪽 상단에 Button이 보이고 하단에 Controls라는 메뉴에 Name, children, size, color, backgroundColor, fontSize, fontWeight, onClick 이라는 속성안에 라디오 버튼이나, 입력창이 보이는데요.

속성값들을 바꿔보고, 입력창에 다른 것을 입력하면 Button UI에 바로 반영이 될겁니다.

개발을 몰라도, 코드 없이 이제 디자이너와 기획자같은 비개발 직군들도 우리가 진행할 프로젝트의 UI를 테스트 할 수 있는 것이죠.

이제 제가 재사용성을 고려해서 버튼을 구현한 것을 Storybook으로 구현했던, Button 컴포넌트와 Storybook 파일의 코드를 보도록 하겠습니다.

import * as S from "./styles"

interface ButtonProps {
  size: "auto" | "rectangle"
  children?: string | React.ReactNode
  color?: "black" | "white"
  backgroundColor?: "none" | "brown"
  fontSize?: "large" | "medium" | "small"
  fontWeight?: "default" | "bold"
  onClick?: () => void
}

const Button = ({
  size,
  children,
  color = "black",
  backgroundColor = "none",
  fontSize = "small",
  fontWeight = "default",
  ...props
}: ButtonProps) => {
  return (
    <S.Button
      size={size}
      color={color}
      backgroundColor={backgroundColor}
      fontSize={fontSize}
      fontWeight={fontWeight}
      type='button'
      {...props}
    >
      {children}
    </S.Button>
  )
}

export default Button

먼저 Button 컴포넌트 입니다. 재사용성을 고려해서, 현재 프로젝트에서 사용할 스타일 값들을 props로 받을 수 있게 구현했습니다.

현재 이 타입들이 정답은 아니고, 제가 구현할 프로젝트에서 사용할 것 같은 속성들을 미리 정의를 해서 어디서든 재사용할 수 있는 Button 컴폰너트를 구현할 것 이죠.

import { ComponentStory, ComponentMeta } from "@storybook/react"

import Button from "./index"

export default {
  title: "Button",
  component: Button,
} as ComponentMeta<typeof Button>

const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />

export const Default = Template.bind({})

Default.args = {
  children: "Button",
}

export const Rectangle = Template.bind({})

Rectangle.args = {
  children: "장바구니",
  size: "rectangle",
  backgroundColor: "brown",
  color: "white",
  fontSize: "large",
  fontWeight: "bold",
}

다음은 Storybook으로 구현한 Button입니다. Storybook 이미지를 보면 Button 메뉴 안에 DefaultRectangle이라는 하위 메뉴가 보이는데 위 처럼 bind해서 미리 Button안에 속성들을 제가 임의로 설정했고, 배포된 페이지에 속성들을 맘대로 테스트 할 수 있는 것이죠.

스토리북 튜토리얼을 보시면 다 따라할 수 있을겁니다.

회고

아직 CDD방식으로 개발하는 것에 익숙하지 않고, 많은 노력이 필요할 것 같다.

Storybook도 튜토리얼이 4개정도 있어 다 보지는 못했고 숙련이 필요하다.

다른 JestRTK 등의 테스토도 많이 해보지 않아 많이 부족하다...

공부를 할 수록 계속 배워야 할게 산넘이로 쌓이니.. 개발자는 평생 공부한다는게 쉽지가 않다.

그래도 계속 하다보면, 기술에 익숙해지고, 새로운 기술이나, 버전이 업데이트 되어도, 금방 적응하는 날이 오겠지.

Storybook이나 CDD를 통해서 디자이너, 기획자 등의 비개발 직군 분들과 좀더 의사소통이 잘될거같았고, 내 생각엔 협업에 효율을 높이기 위해 이러한 방식을 사용하는게 좋은 것 같다.

참고 자료