나이키 프로젝트 리팩토링

이승훈·2022년 12월 18일
1

시행착오

목록 보기
8/23
post-thumbnail

😀 들어가기전에

잘 하는 프론트엔드 개발자가 되고싶다는 마음이 커지는 요즘이다.
어떻게하면 잘 하는 프론트엔드 개발자가 될 수 있을지 고민하던 중 근래에 2가지에 꽃혀있다.

  1. 깔끔한 코드 작성
  2. 훌륭한 퍼포먼스를 보여주는 웹 어플리케이션

상기의 2가지에 대한 고민을 하며 이전에 작업했던 나이키 프로젝트를 리팩토링하였고 그 과정에 대한 기록이다.
또한 새로운 마음으로 새로운 프로젝트를 시작해볼까라는 생각도 들었지만 기존의 코드를 다시 재구성하고 디벨롭하는 경험이 더 중요할것이라 판단 이전의 프로젝트를 리팩토링하는것으로 결정하였다.

😀 함수 분리

상품목록 페이지에서 보여지는 상품목록은 유저가 선택한 필터링 조건에 따라 서버에 요청 후 띄워주게 된다.
서버에 필터링 조건을 전송에는 쿼리스트링을 사용하였다.

분석

기존 코드

import React, { useEffect, useRef, useState } from 'react';
.
.
function ItemList() {
.
.
  useEffect(() => {
    
//// 1. state 값을 가져와 서버에 요청할 쿼리스트링 생성 
    const sortStandardForSubmit = standardObject[sortStandard];
    let urlForSubmit = `offset=${offset}&limit=${limit}&sort=${sortStandardForSubmit}&`;

    selectedSize.map(size => (urlForSubmit = urlForSubmit + `size=${size}&`));
    selectedColor.map(
      color => (urlForSubmit = urlForSubmit + `color=${color}&`)
    );
    for (const checkListName in checkList) {
      checkList[checkListName].map(
        // eslint-disable-next-line no-loop-func
        checkedList =>
          (urlForSubmit = urlForSubmit + `${checkListName}=${checkedList}&`)
      );
    }
    setSearchParams(urlForSubmit);
    
//// 2. 생성한 쿼리스트링을 통해 서버에 요청 및 응답받은 데이터로 렌더링 지시
    fetch(`${IP_CONFIG}/products?` + urlForSubmit)
      .then(response => response.json())
      .then(result => {
        const { current } = itemListCount;
        const inputItemCount = current
          ? result.list.length - current.children.length
          : 0;

        setProducts(prev => result.list);
      });
  }, [offset, limit, checkList, selectedColor, selectedSize, sortStandard]);

  return (
    <section className="itemList">
      <ListHeader .../>
      <div className="itemListMain">
        <FilterBar .../>
        <ListContent .../>
      </div>
    </section>
  );
}

export default ItemList;

상기의 useEffect안의 코드를 통해 상품목록 렌더링이 수행된다.
하나의 함수 안에 너무 많은 일처리가 발생하며 그것을 구분하면 아래의 2단계로 나뉜다.

  1. state 값을 가져와 서버에 요청할 쿼리스트링 생성
  2. 생성한 쿼리스트링을 통해 서버에 요청 및 응답받은 데이터로 렌더링 지시

1단계는 사용자의 요청을 처리하기 위한 비즈니스로직
2단계는 비즈니스 로직으로 처리된 데이터를 화면에 띄우는 UI 로직이라 판단 하였고,
비즈니스 로직을 별도로 분리하여 코드의 가독성 및 유지보수성을 개선해보자 결정하였다.

개선 내용

쿼리스트링을 구하는 로직을 담당하는 GetQueryString 함수를 별도로 작성 후 하기와같이
useEffect안에선 현재 상태를 입력 후 제출하기 위한 쿼리스트링만을 return하게 해주었다.

개선 코드

useEffect(() => {
  
//// 1. state 값을 가져와 서버에 요청할 쿼리스트링 생성 
  const { queryString } = new GetQueryString(
    selectedSize,
    selectedColor,
    checkList,
    offset,
    limit,
    sortStandard
  );
//// 2. 생성한 쿼리스트링을 통해 서버에 요청 및 응답받은 데이터로 렌더링 지시
  fetch(`${IP_CONFIG}/products?${queryString}`)
    .then(response => response.json())
    .then(({ list }) => {
    dispatch(setItemList(list));// <- 리덕스 적용으로 인한 변경 그러나 데이터 렌더링 지시는 동일
  })
    .catch(error => console.log(error));
}, [offset, limit, checkList, selectedColor, selectedSize, sortStandard]);

쿼리스트링 생성을 담당하는 GetQueryString 함수는 아래와 같다.
함수를 나누게 되면서 코드의 길이는 더 길어지게 되어 이것이 옳은것이지 고민해보았다.
허나 코드의 길이가 길어지더라도 각각의 필터 조건의 상태 별 쿼리스트링을 구하기 위한 로직을 분리한 점은
추후 코드의 유지보수에 더욱 유리할 것이라 판단하였다.

class GetQueryString {
  public queryString: string;

  constructor(
    selectedSizeInput: Array<string>,
    selectedColorInput: Array<string>,
    checkListInput: CheckList,
    offsetInput: number,
    limitInput: number,
    sortStandardInput: string
  ) {
    let quertString = '';
    quertString += GetQueryString.getSortOptionString(
      offsetInput,
      limitInput,
      sortStandardInput
    );
    quertString += GetQueryString.selectSizeGetterForUrl(selectedSizeInput);
    quertString += GetQueryString.selectColorGetterForUrl(selectedColorInput);
    quertString += GetQueryString.checkListGetterForUrl(checkListInput);

    this.queryString = quertString;
  }

  static selectSizeGetterForUrl(selectedSizeInput: Array<string>) {
    let selectSizeForUrl = '';
    selectedSizeInput.forEach(size => {
      selectSizeForUrl += `size=${size}&`;
    });
    return selectSizeForUrl;
  }

  static selectColorGetterForUrl(selectedColorInput: Array<string>) {
    let selectColorForUrl = '';
    selectedColorInput.forEach(color => {
      selectColorForUrl += `color=${color}&`;
    });
    return selectColorForUrl;
  }

  static checkListGetterForUrl(checkListInput: CheckList) {
    let checkListForUrl = '';
    const checkListNames = Object.keys(checkListInput);
    checkListNames.forEach(checkListName => {
      checkListInput[checkListName].forEach(checkedList => {
        checkListForUrl += `${checkListName}=${checkedList}&`;
      });
    });
    return checkListForUrl;
  }

  static getSortOptionString(
    offsetInput: number,
    limitInput: number,
    sortStandardInput: string
  ) {
    const sortStandardForSubmit = sortstandard[sortStandardInput];
    return `offset=${offsetInput}&limit=${limitInput}&sort=${sortStandardForSubmit}&`;
  }
}

😀 URL을 통한 상품 목록 렌더링 기능 추가

상품 목록 페이지를 제작하며 생각했던 주요 기능은 2가지 였다.

  1. 다중 필터링 조건에 따라 유저가 원하는 상품목록 렌더링
  2. 유저가 타인에게 현재 필터링된 상품목록이 있는 페이지 공유 가능

다급한 프로젝트 스케쥴로 인해 1번까지만 기능구현을 하였었고 이번 기회에 2번기능도 추가해주었다.
2번 기능은 URL에 기록된 쿼리스트링을 해석하여 컴포넌트의 상태를 세팅한 후 서버에 해석한 정보를 기반으로 데이터를 요청하는 기능이다.
이를 도식화하면 아래와 같다.

기존 코드

없음 처음 도입.

개선 코드

  const [searchParams] = useSearchParams(); // 브라우저 URL로 부터 쿼리스트링 get

  const dispatch = useDispatch();
  useEffect(() => {
    // 제공 받은 쿼리스트링으로 각각의 필터링 조건 setState
    const filterOptns = getFilterOptionsFromQueryList(searchParams);
    if (filterOptns.size) setSelectedSize([...filterOptns.size]);
    if (filterOptns.color) setSelectedColor([...filterOptns.color]);
    if (filterOptns.checkList) setCheckList({ ...filterOptns.checkList });
    if (filterOptns.offset[0]) setOffset(filterOptns.offset[0]);
    if (filterOptns.limit[0]) setLimit(filterOptns.limit[0]);
    if (filterOptns.sort[0]) {
      setSortStandard(getKeyByValue(filterOptns.sort[0] as string));
    }
    return () => {
      dispatch(setItemList([]));
    };
  }, []);

useEffect(() => {
  
//// state 값을 가져와 서버에 요청할 쿼리스트링 생성 
  const { queryString } = new GetQueryString(
    selectedSize,
    selectedColor,
    checkList,
    offset,
    limit,
    sortStandard
  );
//// 생성한 쿼리스트링을 통해 서버에 요청 및 응답받은 데이터로 렌더링 지시
  fetch(`${IP_CONFIG}/products?${queryString}`)
    .then(response => response.json())
    .then(({ list }) => {
    dispatch(setItemList(list));
  })
    .catch(error => console.log(error));
}, [offset, limit, checkList, selectedColor, selectedSize, sortStandard]);

😀 렌더링 횟수 줄이기

상품 페이지 렌더링을 할 때 마다 fetch를 통해 상품목록을 가져온 후 렌더링 후 한번 더 렌더링 하는 현상을 발견

원인 분석

  1. 필터링 조건들이 저장되어있는 state들을 이용하여 fetch 요청을 위한 쿼리스트링을 작성
  2. 작성한 쿼리스트링 setSearchParams를 통해 URL 변경 (리렌더링!)
  3. 작성한 쿼리스트링 이용하여 서버에 fetch 요청
  4. 서버로 부터 응답받은 데이터로 상품목록 상태 데이터 변경 (리렌더링!)

위와같은 단계를 거쳤다 예상치 못한 추가 렌더링이 발생하여 원인을 찾고자 하였고
setSearchParams로 URL 변경 시 페이지가 리렌더링 된다는 사실을 발견

기존 코드

import React, { useEffect, useRef, useState } from 'react';
.
.

  useEffect(() => {
    const filterOptns = getFilterOptionsFromQueryList(searchParams);
    if (filterOptns.size) setSelectedSize([...filterOptns.size]);
    if (filterOptns.color) setSelectedColor([...filterOptns.color]);
    if (filterOptns.checkList) setCheckList({ ...filterOptns.checkList });
    if (filterOptns.offset[0]) setOffset(filterOptns.offset[0]);
    if (filterOptns.limit[0]) setLimit(filterOptns.limit[0]);
    if (filterOptns.sort[0]) {
      setSortStandard(getKeyByValue(filterOptns.sort[0] as string));
    }
  }, []);

  useEffect(() => {
    const { queryString } = new GetQueryString(
      selectedSize,
      selectedColor,
      checkList as any,
      offset,
      limit,
      sortStandard
    );
    try {
      fetch(`${IP_CONFIG}/products?${queryString}`)
        .then(response => response.json())
        .then(result => {
          setSearchParams(queryString);
          dispatch(setItemList(result.list));
        });
    } catch (error: any) {
      console.log(error);
    }
  }, [offset, limit, checkList, selectedColor, selectedSize, sortStandard]);
  return (
    .
    .
  );
}

export default ItemList;

개선 코드

setSearchParams 제거

import React, { useEffect, useRef, useState } from 'react';
.
.

  useEffect(() => {
    const filterOptns = getFilterOptionsFromQueryList(searchParams);
    if (filterOptns.size) setSelectedSize([...filterOptns.size]);
    if (filterOptns.color) setSelectedColor([...filterOptns.color]);
    if (filterOptns.checkList) setCheckList({ ...filterOptns.checkList });
    if (filterOptns.offset[0]) setOffset(filterOptns.offset[0]);
    if (filterOptns.limit[0]) setLimit(filterOptns.limit[0]);
    if (filterOptns.sort[0]) {
      setSortStandard(getKeyByValue(filterOptns.sort[0] as string));
    }
  }, []);

  useEffect(() => {
    const { queryString } = new GetQueryString(
      selectedSize,
      selectedColor,
      checkList as any,
      offset,
      limit,
      sortStandard
    );
    try {
      fetch(`${IP_CONFIG}/products?${queryString}`)
        .then(response => response.json())
        .then(result => {
          setSearchParams(queryString);
          dispatch(setItemList(result.list));
        });
    } catch (error: any) {
      console.log(error);
    }
  }, [offset, limit, checkList, selectedColor, selectedSize, sortStandard]);
  return (
    .
    .
  );
}

export default ItemList;

그러나

유저가 타인에게 현재 상품리스트들이 정렬된 페이지를 공유할 URL을 얻을 수 없음.
하여 현재 필터링조건이 추가된 URL을 복사 할 수 있는 버튼을 추가하여 타인에게 현재 필터링된 상품목록 페이지를 공유할 수 있게함.

// 링크 공유 버튼 클릭 시 현재 조건들의 state를 통하여 queryString 생성 후 클립보드에 복사
  const copyCurrentListUrl = () => {
    const { queryString } = new GetQueryString(
      selectedSize,
      selectedColor,
      checkList as any,
      offset,
      limit,
      sortStandard
    );
    navigator.clipboard
      .writeText(`localhost:3000/item-list?${queryString}`)
      .then(() => {
        alert('복사되었지롱');
      });
  };

하지만...

라이트 하우스로 렌더링 개선 전 과 후를 비교해보았다.
허나 그 차이는 미비하였다.

개선 전

개선 후

아니 사실 없다고 봐도 된다.
LCP 점수가 엉망징창이고 이부분에 대해 추가적으로 공부하여 다음 포스팅을 개시해볼 계획이다.
두보고자 이녀석.

🌝 추가

라이트하우스가 제공하는 지표를 기반으로 웹어플리케이션 Performance 개선 리팩토링 과정은 이곳 에 이어 나감.

profile
Beyond the wall

0개의 댓글