본문 바로가기
Developer/Javascript 해부학

컴포넌트 상태 변이 흐름에 대한 고찰

by 해적거북 2023. 12. 9.
728x90

목차

  1. 도대체 어디서 상태 변이시키는거야?
  2. 상태 변이 흐름 개선하기
    1. 상태 변이 일원화
    2. 상태 돌려놓기
    3. 부적절한 UX
  3. 마무리

도대체 어디서 상태 변이시키는거야?

why_why_why_ohThatsWhy

컴포넌트 상태가 복잡해져서 예상치 못한 변이 흐름이 생기지 않을까 고민하셨나요? 아니면 의도하지 않은 상태로 변이되어서 당황하신 적이 있으신가요? 컴포넌트에 기능이 많아지고 비즈니스 로직이 복잡해질 수록 더 많은 상태가 생기게 됩니다. 특히 api 호출에 의존되는 경우 예외처리까지 고려해야합니다. 생각만해도 사이드 이펙트로 화면에 이상한 값이 보여질까 걱정되는데 여러분들은 어떻게 상태 변이 흐름을 관리하시나요?


상태 변이 흐름 개선하기

먼저 상황을 가정해보겠습니다. 화면은 필터링, 상품의 목록과 페이지네이션으로 이루어져있습니다. 첫 화면이 렌더링이 완료되면 api 호출로 상품 목록을 가져와 보여줍니다. 그리고 필터값이 변경되거나 page이동할때 상품 목록을 호출하게 됩니다.

// 컴포넌트 상태의 흐름에 집중하기 위해 그 외 코드는 제외했습니다. (로딩, 예외처리, 디자인 등)

export default function Before() {
  const [page, setPage] = useState(0);
  const [productList, setProductList] = useState([]);
  const [selectFilter, setSelectFilter] = useState("price");

  const getProductList = useCallback(async () => {
    const { products } = await API.getProductList(page, selectFilter);
    setProductList(products);
  }, [page, selectFilter]);

  useEffect(() => {
    getProductList();
  }, [page, selectFilter, getProductList]);

  return (
    <section>
      <select value={selectFilter} onChange={(e) => setSelectFilter(e.target.value)}>
        <option value="price">price</option>
        <option value="stock">stock</option>
        <option value="brand">brand</option>
      </select>

      <ul>
        {productList.map(({ id, title, price, stock, brand }) => (
          <li key={id}>
            {id} : {title} : {price || stock || brand}
          </li>
        ))}
      </ul>

      <div>
        <button onClick={() => setPage((prev) => prev - 2)}>{page - 2}</button>
        <button onClick={() => setPage((prev) => prev - 1)}>{page - 1}</button>
        <span>{page}</span>
        <button onClick={() => setPage((prev) => prev + 1)}>{page + 1}</button>
        <button onClick={() => setPage((prev) => prev + 2)}>{page + 2}</button>
      </div>
    </section>
  );
}

복잡하지 않은 기능과 동작이라 큰 문제가 없어보입니다. 하지만 저는 이부분에서 3가지가 눈에 들어와서 개선해보았습니다.


상태변이 일원화

첫번째는 컴포넌트 상태 변이 호출하는 곳이 많다는 것입니다. 현재 상태 변이를 유발하는 곳은 3곳입니다. 필터 선택, 페이지네이션 변경, api 응답값을 가져오는 productList에서 상태 변이를 일으킵니다. 이를 도식화하면 다음 그림과 같습니다. 참고로 화살표의 순서는 무지개색 순서와 동일하게 보시면 됩니다.

상태변이_도식화_before

만약 해당 State를 변이시키는 Event와 API가 더 많아지면 어떻게 될까요? State의 변이 흐름이 점점 복잡해질 것 같습니다. 또한 의도하지 않은 State가 보여질때는 디버깅하기도 간단할 것 같지 않습니다.


그럼 상태 변이를 하는 곳을 줄이고자 Event의 동작을 State 변이가 아닌 API 호출로 하면 어떨까요? 이부분도 도식화하여 그림으로 가져와보았습니다. 위와 동일하게 화살표 순서는 무지개색 순서와 동일합니다.

상태변이_도식화_after

State를 향한 화살표의 갯수도 줄었지만, 전체적인 화살표의 갯수와 흐름도 간단해졌습니다. Event에서 DOM에 반영되기까지 흐름도 간결해지니 디버깅하기도 훨씬 수월해보입니다.


상태 돌려놓기

두번째는 예외처리를 위한 상태 돌려놓기입니다. 위 상태 변이(Before) 그림에서 API 호출 오류가 나면 어떻게 될까요? API를 호출할때는 이미 page 또는 select를 변경시켜놓은 상태라서 화면에 보여지는 page, select에 맞지 않은 productList를 보여주고 있습니다. 따라서 기본적인 API 예외처리 동작에 상태를 돌려놓는 동작도 필요해집니다. 하지만 상태 변이 (After) 그림에서 API 호출 오류가 생겨도 기본적인 예외처리만 하면 됩니다. 왜냐하면 State에는 아무런 변이가 없어 화면은 page, select에 알맞은 productList를 보여주고 있기 때문입니다.


부적절한 UX

세번째는 올바른 데이터를 화면에 보여주는가 입니다. 첫번째와 두번째를 개발자 입장에서 고려한 부분이고, 세번째는 사용자 입장에서 고려한 부분입니다. page이동한 화면을 보여주기 위해 page 상태 변이에 의한 렌더링, productList 상태 변이에 의한 렌더링 총 2번 렌더링하게 됩니다. 또한 각각의 렌더링 사이에서 사용자는 적절하지 않은 화면을 보게 됩니다. (page에 맞지 않은 productList)

상태_변이_before

Event에 대한 동작을 State가 아닌 API 호출로 바꾸면 사용자에게 항상 적절한 화면을 보여주게 됩니다.

상태_변이_after

결과적으로 3가지 부분을 개선한 코드는 아래와 같습니다.

// 컴포넌트 상태의 흐름에 집중하기 위해 그 외 코드는 제외했습니다. (로딩, 예외처리, 디자인 등)

export default function After() {
  const [page, setPage] = useState(0);
  const [productList, setProductList] = useState([]);
  const [selectFilter, setSelectFilter] = useState("price");

  const getProductList = async (newPage = page, newSelectFilter = selectFilter) => {
    const { products } = await API.getProductList(newPage, newSelectFilter);
    setProductList(products);
    setPage(newPage);
    setSelectFilter(newSelectFilter);
  };

  useEffect(() => {
    getProductList();
  }, []);

  return (
    <section>
      <select value={selectFilter} onChange={(e) => getProductList(page, e.target.value)}>
        <option value="price">price</option>
        <option value="stock">stock</option>
        <option value="brand">brand</option>
      </select>

      <ul>
        {productList.map(({ id, title, price, stock, brand }) => (
          <li key={id}>
            {id} : {title} : {price || stock || brand}
          </li>
        ))}
      </ul>

      <div>
        <button onClick={() => getProductList(page - 2)}>{page - 2}</button>
        <button onClick={() => getProductList(page - 1)}>{page - 1}</button>
        <span>{page}</span>
        <button onClick={() => getProductList(page + 1)}>{page + 1}</button>
        <button onClick={() => getProductList(page + 2)}>{page + 2}</button>
      </div>
    </section>
  );
}

마무리

서비스가 고도화 될수록 컴포넌트 간의 관계가 복잡해지며 상태 변이를 유발하는 곳은 많아지게 됩니다. 그럴수록 변이 흐름을 잘 제어해야 서비스가 더 탄탄해질거라 생각합니다. 필자가 소개한 방법은 정답이 아닙니다. 하나의 방법론일 뿐이며 더 좋은 방법론을 알게 된다면 또 소개하고자 글 쓸 생각입니다.

728x90

댓글