[리팩토링] StoryBook

yoon Y·2022년 2월 8일
1

[3rd_Project] MonthSub

목록 보기
9/11

공식문서 학습

const Template = args => <Task {...args} />;

export const Default = Template.bind({});
Default.args = {
  task: {
    id: '1',
    title: 'Test Task',
    state: 'TASK_INBOX',
    updatedAt: new Date(2018, 0, 1, 9, 0),
  },
};
  • Template.bind({})는 함수의 복사본을 만드는 표준 JavaScript의 한 기법.
    => 스토리가 각각 다른 속성을 갖지만 동시에 동일한 구현을 할 수 있음.

  • 스토리를 만들기 위해 함수를 내보내는데, 그 함수의 args속성에 props값들을 할당해주면 args를 인자로 받아 해당props가 포함된 컴포넌트를 반환한다. (args === props)

  • props이 아닌 args로 받는 이유는 컨트롤러를 사용한 실시간 컴포넌트 업데이트 때문.

  • 다른 story파일에서도 쓸 수 있도록 각 스토리를 export해준다.

  • 데코레이터(Decorators)는 스토리에 임의의 래퍼(wrapper)를 제공하는 한 방법 -> provider도 적용할 수 있음

export default {
  component: TaskList,
  title: 'TaskList',
  decorators: [story => <div style={{ padding: '3rem' }}>{story()}</div>],
};

학습 자료

Presentational and Container Components
스토리북 공식 문서


리팩토링

  • 데이터가 필요한 컴포넌트는 api에서 불러온 데이터가 아닌 더미를 넣어준다
  • args는 props에 컨트롤을 달아줄 때 사용하고, 컨트롤이 필요 없는 경우에는 그냥 props처럼 넣어줘도 된다
  • export default에 정의해주는 argtypes는 모든 스토리의 공통으로 args로 들어간다
  • 버전이 2개 이상 필요할 땐 템플릿을 만들고 arg를 따로 지정한다
    → 다른 중복 코드(마크업) 작성할 필요없이 딱 props만 작성할 수 있기 때문

🧐 스토리북 와장창의 이유를 알았다..

Page단위 컴포넌트 외에 재사용되는 컴포넌트도 외부 영향(api, context)을 받아서 그랬던 것.. 스토리북을 작성하려면 순수 컴포넌트여야만 한다

참고자료


🧐 useForm훅을 쓸 경우 storybook에 어떻게 작성할까?

제출 api가 실행되는 로직과 initialvalue를 페이지 단에서 props으로 받는다

위의 내용을 SeriesForm컴포넌트에 적용

[구조]
Write/EditSeriesForm(컴포넌트) > seriesForm(컴포넌트) > useForm(훅)

[문제점]

  • useForm훅의 props인 onSubmit함수는 훅 내부에서 useForm의 values(상태)를 파라미터로 받아 서버에 전송하는 로직이다.
  • useForm의 handleChange를 input의 name과 value로만 받아야하다는 고정 관념 때문에
    그렇게 받아질 수 없는 값은 seriesForm에 따로 상태를 만들었다
  • 이랬을 경우 문제점은 useForm의 onSubmit로직을 최상위 page컴포넌트에서 작성해 내려줄 수가 없었다
    useForm까지 onSubmit함수를 전달해서 useForm의 상태인 values를 parameter로 받아서 로직을 처리해야하는데, seriesForm에 따로 상태를 만들었을 경우 values에 해당 값이 없기 때문이다
  • 이를 해결하기 위해서는 seriesForm에 따로 만든 상태를 useForm의 values에 포함시켜야했다
    → useForm에서 prameter로 값을 받는 핸들러 함수를 추가해주어 target내부의 값(name, value)을 받지 못하는 값들도 useForm의 state에 저장될 수 있도록 했다
  const handleChangeArr = (name, value) => { // prameter로 값을 받는 함수 추가
    setValues({ ...values, [name]: value });
  };

  const handleChange = e => { // 기존의 click target내부의 값을 받는 핸들러함수
    const { name, value } = e.target;
    setValues({ ...values, [name]: value });
  };

[깨달은 점]

구조가 Component - Component - hook으로 되어있을 때 props이 내려지는 구조와 과정을 잘 이해하지 못했던 것 같다. (특히 핸들러이벤트를 내려줄 때)

  • props으로 받는 handler함수는 외부의 로직을 가져와서 하위 컴포넌트에서 실행하는 것
  • 상위 컴포넌트가 하위 컴포넌트의 상태를 직접 이용할 수 없기 때문
  • 상위 컴포넌트가 하위 컴포넌트에게 명령하는 것(?)
  • 바꾸고 싶은 상태가 속해있는 컴포넌트(또는 훅)에서 onChange함수를 실행시킴

외부 영향을 받는 컴포넌트와 순수한 컴포넌트의 범위와 기준을 정해야한다

  • Presentational/Container 컴포넌트 : 컴포넌트 재사용 관점
    • Container: api호출이나 전역 상태 관리 로직이 실행되는, sideEffect가 발생할 수 있는 컴포넌트
      • 재사용 불가능
      • page나 wrapped컴포넌트가 적합
    • Presentational: 데이터나 이벤트 핸들러함수를 props으로 받아 처리하는, sideEffect가 발생하지 않는 컴포넌트
      • 재사용 가능
      • 스토리북에 작성하기 좋음
  • React Hook: 로직 재사용 관점

참고 자료


🧐 Depth가 깊고 사용 빈도가 높은 컴포넌트를 순수한 컴포넌트로 만들어야할까?

LikeToggle컴포넌트에 api호출, contextApi로직이 포함되어있어서 storyBook에 작성할 때 오류가 났다
page컴포넌트에서 호출한 후 props으로 내려줘야했는데 depth가 너무 깊고, 거의 모든 page에서 사용 중이었기 때문에 비효율적이라는 생각이 들었다

LikeToggle폴더에 내부 컴포넌트로 wrapped컴포넌트를 만들어서 contextApi를 호출하고 받은 값과, state에 따라 좋아요 추가, 취소 api를 실행하는 handler함수(+그 외 필요한 값들)를 LikeToggle컴포넌트에 Props으로 넘겨주었다.

sideEffect가 발생할 수 있는 로직들을 분리해서 상위 컴포넌트로 감싸준 것.
storyBook에는 LikeToggle컴포넌트만 불러와서 직접 props값을 넣어 사용하면 된다

// LikeToggle/Wrapped.jsx - Container 컴포넌트
export const LikeToggleWrapped = ({ id, isLiked, likeCount }) => {
  const { userInfo } = useUser();

  const handleClick = async state => {
    state ? await delLikeSeries(id) : await addLikeSeries(id);
  };

  return (
    <Like
      seriesId={id}
      isLogin={userInfo.userId}
      isLiked={isLiked}
      likeCount={likeCount}
      onClick={handleClick}
    />
  );
};
// LikeToggle/index.jsx - Presentational 컴포넌트
export const LikeToggle = ({ isLogin, isLiked, likeCount, onClick }) => {
  const [state, toggle] = useToggle();
  const [count, setCount] = useState(0);

  useEffect(() => {
    isLiked && toggle();
    setCount(likeCount);
  }, [likeCount, isLiked]);

  const addLike = async () => {
    setCount(count + 1);
    onClick && onClick(state);
  };

  const cancleLike = async () => {
    setCount(count - 1);
    onClick && onClick(state);
  };

  const handleClick = () => {
    if (!isLogin) {
      return;
    }
    toggle();
    state ? cancleLike() : addLike();
  };

  return (
    <Container onClick={handleClick}>
      <IconWrapper color={isLogin ? theme.color.red : theme.color.gray}>
        {state ? <Icon.Like /> : <Icon.LikeBorder />}
      </IconWrapper>
      {typeof likeCount === 'boolean' ? '' : <Count>{count}</Count>}
    </Container>
  );
};

하지만 문제는 후에 기능을 추가하면 댓글이나 다른 컨텐츠에도 좋아요 기능이 있어야 할 텐데 지금의
Wrapped에는 시리즈 좋아요 api만 작성했었다
1. api마다 wrapped컴포넌트를 만든다
2. 현재의 wrapped컴포넌트에 모든 좋아요 관련 api를 작성해놓고 props을 받아 조건 실행한다
두 가지 방법이 있었지만 다 마음에 들지 않았다..

결국은 LikeToggleWrapped를 SeriesLikeToggle이라고 이름을 변경했다
SeriesLikeToggle에는 시리즈 좋아요 관련 api만 작성되어있고,
후에 댓글 부분이나 다른 부분에 LikeToggle이 필요하다면 그때는 시리즈 만큼 많은 페이지에서 쓰이지 않을 것이기 때문에 순수한 LikeToggle를 사용해 page단 컴포넌트에서 api와 context를 props으로 받아서 사용해도 될 것 같다고 생각했기 때문이다.




느낀점

스토리북 리팩토링을 시작하고부터 많은 컴포넌트를 다시 수정했다.
컴포넌트만 리팩토링할 때보다 고칠 점이 더 잘 보였다.
스토리북에 등록하려면 컴포넌트를 순수하게 잘 짜야한다는 것을 느꼈다...

profile
#프론트엔드

0개의 댓글