티스토리 뷰

Assemble a composite component

Last chapter we built our first component; this chapter extends what we learned to build TaskList, a list of Tasks. Let’s combine components together and see what happens when more complexity is introduced.

=> 지난 챕터에서 첫 번째 컴포넌트를 만들었습니다; 이번 챕터에서는 Task들을 리스트하는 TaskList를 만드는 것으로 확장 합니다. 컴포넌트들을 결합하여 함께할 때, 어떤 일이 발생하는지 봅시다.

Tasklist

Taskbox emphasizes pinned tasks by positioning them above default tasks. This yields two variations of TaskList you need to create stories for: default items and default and pinned items.

=>Taskbox는 고정된 작업들을 기본작업들을 위에 배치하므로써 강조를 합니다. 그러면 기본항목 과 기본항목 및 고정항목에 대한 스토리를 작성하는데 필요한 TaskList의 두가지 변형이 생성됩니다.

Since Task data can be sent asynchronously, we also need a loading state to render in the absence of a connection. In addition, an empty state is required when there are no tasks.

=> Task 데이터는 비동기적으로 전송되어지기 때문에, 커넥션이 없는 경우 렌더링이 필요하고, 게다가 tasks가 없을 때 빈 state 값이 필요한다.

Get setup

A composite component isn’t much different than the basic components it contains. Create a TaskList component and an accompanying story file: src/components/TaskList.js and src/components/TaskList.stories.js.

Start with a rough implementation of the TaskList. You’ll need to import the Task component from earlier and pass in the attributes and actions as inputs.

=> 복합 컴포넌트는 자기가 갖고 있는 기존 컴포넌트와 크게 다르지 않는다. TaskList 컴포넌트와 수반되는 src/components/TaskList.js 와 src/components/TaskList.stories.js를 만듭니다.

// src/components/TaskList.js

import React from 'react';

import Task from './Task';

function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
  const events = {
    onPinTask,
    onArchiveTask,
  };

  if (loading) {
    return <div className="list-items">loading</div>;
  }

  if (tasks.length === 0) {
    return <div className="list-items">empty</div>;
  }

  return (
    <div className="list-items">
      {tasks.map(task => <Task key={task.id} task={task} {...events} />)}
    </div>
  );
}

export default TaskList;

Next create Tasklist’s test states in the story file.

≫ 다음으로 스토리 파일에 Tasklist 테스트의 상태를 만들자.

// src/components/TaskList.stories.js

import React from 'react';
import { storiesOf } from '@storybook/react';

import TaskList from './TaskList';
import { task, actions } from './Task.stories';

export const defaultTasks = [
  { ...task, id: '1', title: 'Task 1' },
  { ...task, id: '2', title: 'Task 2' },
  { ...task, id: '3', title: 'Task 3' },
  { ...task, id: '4', title: 'Task 4' },
  { ...task, id: '5', title: 'Task 5' },
  { ...task, id: '6', title: 'Task 6' },
];

export const withPinnedTasks = [
  ...defaultTasks.slice(0, 5),
  { id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
];

storiesOf('TaskList', module)
  .addDecorator(story => <div style={{ padding: '3rem' }}>{story()}</div>)
  .add('default', () => <TaskList tasks={defaultTasks} {...actions} />)
  .add('withPinnedTasks', () => <TaskList tasks={withPinnedTasks} {...actions} />)
  .add('loading', () => <TaskList loading tasks={[]} {...actions} />)
  .add('empty', () => <TaskList tasks={[]} {...actions} />);

addDecorator() allows us to add some “context” to the rendering of each task. In this case we add padding around the list to make it easier to visually verify.

Decorators are a way to provide arbitrary wrappers to stories. In this case we’re using a decorator to add styling. They can also be used to wrap stories in “providers” –i.e. library components that set React context.

task supplies the shape of a Task that we created and exported from the Task.stories.js file. Similarly, actions defines the actions (mocked callbacks) that a Task component expects, which the TaskList also needs.

Now check Storybook for the new TaskList stories.

=> 'addDecorator()는 각각의 task의 렌더링에 "context"를 추가할 수 있다. 이 경우 시각적으로 쉽게 확인하기 위해 리스트에 padding을 추가한다.

Decorators스토리들에 임의의 랩퍼를 제공하는 방법이다. 이 경우 styling을 추가하기위해 decorator를 사용한다. 그 것은 또한 "providers"에서 스토리를 감싸는 것으로 사용될 수 있다 - React context를 설정하는 라이브러리 컴포넌트

task는 Task.stories.js 파일에서 만들고 내보내는 Task의 모양을(?) 제공한다. 비슷하게, actions는 TaskList가 필요로하는 Task 컴포넌트의 기대하는 동작들을(mocked callbacks)을 정의한다.

이제 새로운 TaskList 스토리들를 위한 Storybook을 확인해 보자

https://www.learnstorybook.com/inprogress-tasklist-states.mp4

Build out the states

Our component is still rough but now we have an idea of the stories to work toward. You might be thinking that the .list-items wrapper is overly simplistic. You're right – in most cases we wouldn’t create a new component just to add a wrapper. But the real complexity of TaskList component is revealed in the edge cases withPinnedTasks, loading, and empty.

≫ 우리의 컴포넌트는 아직 투박하지만 이제 작업해 나갈 스토리들의 아이디어가 있다. '.list-items' 랩퍼가 매우 단순하다고 생각할 수 있다. 맞다 - 대부분 단지 랩퍼를 추가하기 위해 새로운 컴포넌트를 만들지 않는다. 그러나 TaskList 컴포넌트의 실제 복잡성은 'withPinnedTaks, loading 그리고 empty 경우의 가장자리에 드러난다.

// src/components/TaskList.js

import React from 'react';

import Task from './Task';

function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
  const events = {
    onPinTask,
    onArchiveTask,
  };

  const LoadingRow = (
    <div className="loading-item">
      <span className="glow-checkbox" />
      <span className="glow-text">
        <span>Loading</span> <span>cool</span> <span>state</span>
      </span>
    </div>
  );

  if (loading) {
    return (
      <div className="list-items">
        {LoadingRow}
        {LoadingRow}
        {LoadingRow}
        {LoadingRow}
        {LoadingRow}
        {LoadingRow}
      </div>
    );
  }

  if (tasks.length === 0) {
    return (
      <div className="list-items">
        <div className="wrapper-message">
          <span className="icon-check" />
          <div className="title-message">You have no tasks</div>
          <div className="subtitle-message">Sit back and relax</div>
        </div>
      </div>
    );
  }

  const tasksInOrder = [
    ...tasks.filter(t => t.state === 'TASK_PINNED'),
    ...tasks.filter(t => t.state !== 'TASK_PINNED'),
  ];

  return (
    <div className="list-items">
      {tasksInOrder.map(task => <Task key={task.id} task={task} {...events} />)}
    </div>
  );
}

export default TaskList;

The added markup results in the following UI:

Note the position of the pinned item in the list. We want the pinned item to render at the top of the list to make it a priority for our users.

≫ 추가된 마크업은 다음 UI로 나타난다:

리스트에서 고정된 아이템의 위치를 기록하자. 고정된 항목을 리스트의 상위에 위치시켜 사용자를 위해 우선순위를 주기를 원한다.

Data requirements and props

As the component grows, so too do input requirements. Define the prop requirements of TaskList. Because Task is a child component, make sure to provide data in the right shape to render it. To save time and headache, reuse the propTypes you defined in Task earlier.

≫ 컴포넌트가 커지면, 입력 요구사항도 역시 커진다. TaskList의 요구사항 prop를 정의해보자. Task는 하위 컴포넌트이기 때문에 그것이 렌더링 될 수 있게 올바른 모양(타입)의 데이터를 제공해야한다. 시간을 아끼고 머리아프지 않으려면, 이전 Task에서 정의한 propTypes를 재사용하자.

 

// src/components/TaskList.js

import React from 'react';
import PropTypes from 'prop-types';

function TaskList() {
  ...
}


TaskList.propTypes = {
  loading: PropTypes.bool,
  tasks: PropTypes.arrayOf(Task.propTypes.task).isRequired,
  onPinTask: PropTypes.func.isRequired,
  onArchiveTask: PropTypes.func.isRequired,
};

TaskList.defaultProps = {
  loading: false,
};

export default TaskList;

Automated testing

In the previous chapter we learned how to snapshot test stories using Storyshots. With Task there wasn’t a lot of complexity to test beyond that it renders OK. Since TaskList adds another layer of complexity we want to verify that certain inputs produce certain outputs in a way amenable to automatic testing. To do this we’ll create unit tests using Jest coupled with a test renderer such as Enzyme.

≫ 이전 챕터에서 Storyshots을 이용하여 snapshot 테스트하는 법을 배웠습니다. Task에서는 렌더링을 하는것 뿐만아니라 테스트하기 위해 많은 복잡성이 없었다(?). TaskList는 또다른 복잡성의 레이어를 추가하기 때문에 특정 입력값이 자동 테스트로 처리될 수 있는 정확한 출력값을 제공하는지 검증하길 원한다. Jest 를 사용하는 단위 테스트와 Enzyme 같은 테스트 렌더러가 결합되었습니다.

Unit tests with Jest

Storybook stories paired with manual visual tests and snapshot tests (see above) go a long way to avoiding UI bugs. If stories cover a wide variety of component use cases, and we use tools that ensure a human checks any change to the story, errors are much less likely.

≫ 수동 시각 테스트 및 snapshot 테스트들이(위참조) 결합된 Storybook 스토리들은 UI 버그들을 피하는데 많은 도움이 된다. 만약 스토리들이 광범위하게 다양한 컴포넌트의 사용 케이스를 담고있다면, 스토리의 어떤 변경도 사람이 체크할 수 있는 툴을 사용하면 오류가 적게 발생한다.

However, sometimes the devil is in the details. A test framework that is explicit about those details is needed. Which brings us to unit tests.

≫ 하지만, 때로는 악마가 숨어 있다. 이런 details에 대한 명확한 테스트 프레임워크가 필요하다. 이것은 단위 테스트를 요하게 된다.

In our case, we want our TaskList to render any pinned tasks before unpinned tasks that it has passed in the tasks prop. Although we have a story (withPinnedTasks) to test this exact scenario, it can be ambiguous to a human reviewer that if the component stops ordering the tasks like this, it is a bug. It certainly won’t scream “Wrong!” to the casual eye.

 

≫ 우리의 경우에서, TaskList가 tasks prop가 전달된 비고정된 업무 전에 어떤 고정 업무들을 렌더링하기를 원한다(;). 비록 정확한 시나리오를 테스트하기 위한 스토리(withPinnedTasks)를 가지고 있지만 그것은 만약 컴포넌트가 이렇게 업무 정렬을하는 것을 멈춘다면 휴먼 리뷰어가 모호해 질 수 있다. 이 것은 보통 "잘못됬다"고 외칠 수 있다.

So, to avoid this problem, we can use Jest to render the story to the DOM and run some DOM querying code to verify salient features of the output.

Create a test file called src/components/TaskList.test.js. Here, we’ll build out our tests that make assertions about the output.

≫ 그래서 이문제를 피하기 위해, Jest를 사용하여 DOM에 스토리를 렌더링하고 DOM 쿼리 코드를 실행하여 결과값의 주요 기능을 검증할 수 있다.

'src/components/TaskList.test.js' 파일을 만들자. 여기서는 결과에 대한 assertions 하는 테스트를 만들 것이다.

// src/components/TaskList.test.js

import React from 'react';
import ReactDOM from 'react-dom';
import TaskList from './TaskList';
import { withPinnedTasks } from './TaskList.stories';

it('renders pinned tasks at the start of the list', () => {
  const div = document.createElement('div');
  const events = { onPinTask: jest.fn(), onArchiveTask: jest.fn() };
  ReactDOM.render(<TaskList tasks={withPinnedTasks} {...events} />, div);

  // We expect the task titled "Task 6 (pinned)" to be rendered first, not at the end
  const lastTaskInput = div.querySelector('.list-item:nth-child(1) input[value="Task 6 (pinned)"]');
  expect(lastTaskInput).not.toBe(null);

  ReactDOM.unmountComponentAtNode(div);
});

Note that we’ve been able to reuse the withPinnedTasks list of tasks in both story and unit test; in this way we can continue to leverage an existing resource (the examples that represent interesting configurations of a component) in many ways.

≫ 스토리와 단위 테스트에서 업무 리스트 withPinnedTasks를 재 사용할 있었다. 이러한 방식으로 여러가지 면에서 기존 자원을(컴포넌트의 연관된 구성요소를 나타내는 예) 계속 활용 할 수 있다.

Notice as well that this test is quite brittle. It's possible that as the project matures, and the exact implementation of the Task changes --perhaps using a different classname or a textarea rather than an input--the test will fail, and need to be updated. This is not necessarily a problem, but rather an indication to be careful about liberally using unit tests for UI. They're not easy to maintain. Instead rely on visual, snapshot, and visual regression (see testing chapter) tests where possible.

≫ 또한 이 테스트는 매우 취약하다는 점에 유의하십시오. 프로젝트가 성숙되고 업무의 정확한 구현이 변경 될 수 있습니다 - 만약 input이 아닌 다른 classname 이나 textarea 사용하는 경우 - 테스트가 실패하고 업데이트해야 할 수도 있습니다. 이것은 반드시 문제는 아니지만 오히려 UI에 대해 단위 테스트를 자유롭게 사용하는 것에 주의해야 한다는 표시입니다. 그들은 유지하기가 쉽지 않습니다. 대신 시각, 스냅 샷 및 시각적 회귀 ( testing chapter 참조)에 의존하여 가능하면 테스트하십시오.

'Tool > Storybook' 카테고리의 다른 글

Storybook 적용기  (2) 2019.07.17
[Storybook Doc 번역_v5.1] - Simple Component(React)  (0) 2019.07.16
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함