티스토리 뷰

오래간만에 포스팅입니다!

10월에는 이사, 결혼준비 등 이벤트가 많아서 블로그 작성에 소홀했네요.

이사하고 나서 대중교통에 서있는 시간이 많아졌습니다.

회사까지 거리는 가까워졌지만.. 앉아서 가는 건 불가능해졌어요.

요즘엔 이 기회(?)를 살려 출퇴근 길에 공식문서를 읽는 습관을 들이고 있어요.

요즘엔 리액트 공식문서를 읽고 있는데, 마침 제가 당면한 문제를 해결할 수 있는 힌트를 얻었고

이를 통해 코드를 개선한 경험을 공유하고자 합니다.

 

문제 1. 상태(state)가 복잡하다.

총 4개의 필터데이터를 관리하는 상태입니다.

const initialState = [[],[],[],[]];

const [filter, setFilter] = useState(initialState);

처음에는 상태를 배열로 만들었습니다.

이렇게 하다보니 filter[0][2] 처럼 작성자가 아니면 알아볼 수 없는 코드가 생겼습니다.

다른 개발자에게 민폐끼치기 딱 좋은 코드가 되어버렸죠ㅠㅠ

 

해결책 1. 상태를 배열에서 객체로 바꾼다.

const initialState = {location:[], product:[], unit:[], origin:[]};

const [filter, setFilter] = useState(initialState);

상태를 객체로 바꾸면 filter.location으로 접근하기 때문에 다른 사람도 이해할 수 있는 코드를 작성하게 됩니다. 

 

문제 2. 똑같은 로직을 여러 컴포넌트에서 반복하게 된다.

const LocationFilter = ({ filter, setFilter }) => {
  const handleClick = (id) => {
    setFilter((prev) => {
      if (prev.product.includes(id)) {
        // 제거하는 로직
      } else {
        // 추가하는 로직
      }
    });
  };

  return (
    <div onClick={handleClick}>
      <Filter />
    </div>
  );
};
const ProductFilter = ({ filter, setFilter }) => {
  const handleClick = (id) => {
    setFilter((prev) => {
      if (prev.product.includes(id)) {
        // 제거하는 로직
      } else {
        // 추가하는 로직
      }
    });
  };

  return (
    <div onClick={handleClick}>
      <Filter />
    </div>
  );
};

// ...반복

결국 필터 상태를 관리하는 로직이 크게 달라지지 않는데도 각 컴포넌트에서 동일한 로직을 반복해서 작성하는 일이 반복되었습니다.

만약 필터 종류가 5가지라면 총 5번의 비슷한 코드를 반복해야 합니다.

해결책 2. useReducer를 사용하여 반복되는 로직을 리듀서로 통합한다.

구현에서 한발 물러나서 기능을 단순하게 바라봤습니다.

결국 무엇을 클릭했을때 요소를 더하고 빼거나, 초기화하거나, 업데이트하는 것이 전부였습니다.

배열에서 요소를 더하고 빼는 로직을 작성할때 기존에는 

배열에 요소가 있는지 없는지 체크한후(Array.prototype.includes),

있으면 기존요소에서 빼고(Array.prototype.filter), 없으면 추가하는 로직을 작성했습니다.

이 과정 자체가 계속 순회를 하고 코드도 복잡해졌습니다.

매번 현재 state에서 includes, filter, 전개연산자를 반복해야 하는 것도 피로감이 느껴지고 생산성이 낮아집니다.

이를 해결하기 위해 자바스크립트의 set객체를 사용하면 어떨까 하여 적용해 봤습니다.

set객체를 사용하면 같은 요소가 두 번 추가되는 케이스도 없을 것이고

Set.prototype으로 상속받는 has, add, delete 메서드를 통해

쉽게 중복여부를 체크할 수 있기 때문에 set객체를 사용해서 배열에서 특정요소를 toggle 하는 함수를 만들었습니다.

const toggleState = (state, element) => {
  const newState = new Set(state.map(JSON.stringify));
  const elementStringified = JSON.stringify(element);

  if (newState.has(elementStringified)) {
    newState.delete(elementStringified);
  } else {
    newState.add(elementStringified);
  }

  return Array.from(newState, JSON.parse);
};

JSON부분은 중복된 객체도 비교하기 위해 작성한 코드입니다.

마지막에는 set객체를 다시 배열로 만들어서 리턴하는 코드입니다.

 

이제 배열에 요소를 더하고 빼는 함수가 만들어졌으므로 모든 로직을 통합하는 리듀서를 만들겠습니다

리듀서는 useState를 useReducer로 대체합니다. 둘 다 상태를 관리하는 훅입니다.

useReducer는 복잡한 상태를 관리할 때 유리합니다. 왜냐하면 리듀서에서 복잡하게 반복되는 로직을 통합할 수 있기 때문입니다.

function filterReducer(state, action) {
  switch (action.type) {
    case 'toggle': {
      const newState = toggleState(state[action.key], action.payload);
      return { ...state, [action.key]: newState };
    }
    case 'change': {
      return { ...action.payload };
    }
    case 'init': {
      const newObj = Object.keys(state).reduce((acc, key) => {
        acc[key] = [];
        return acc;
      }, {});

      return newObj;
    }

    default: {
      throw Error(`Unknown action: ${action.type}`);
    }
  }
}

리듀서는 action.type으로 문자열을 받아서 리듀서에 작성된 로직을 수행하고 리턴문의 상태를 반환합니다.

const ProductFilter = ({filter, setFilter}) => {
	const handleClick = (id) => {
    	setFilter(prev=>{
        	if(prev.product.includes(id)){
            	// 제거하는 로직
            }else{
            	// 추가하는 로직
            }
        })
    }
	
	return (
    	<Filter />
    )
}

위 컴포넌트를 아래와 같이 수정할 수 있습니다.

const ParentComponent = () => {
  const [filter, dispatch] = useReducer(filterReducer, initialState);

  return <ProductFilter dispatch={dispatch} filter={filter} />;
};

const ProductFilter = ({ dispatch, filter }) => {
  const handleClick = (id, item) => {
    dispatch({ key: 'product', payload: item, type: 'toggle' });
  };

  return (
    <div onClick={handleClick}>
      <Filter filter={filter} />
    </div>
  );
};

dispatch는 리덕스를 사용해 보셨다면 익숙하실 겁니다. useReducer훅의 액션을 dispatch 하는 역할을 합니다.

handleClick 이벤트핸들러의 내부가 아주 간단해졌습니다.

리듀서에게 'toggle'이라는 이름의 액션을 발생시키기만 했습니다.

이렇게 반복되는 코드를 reducer로 옮기고 나면 dispatch type에 액션만 전달해 주면 되기 때문에

컴포넌트 내부에서 똑같은 로직을 반복해서 작성할 필요가 없어졌습니다.

 

문제 3. props를 '반복'해서 '깊게'전달하게 된다.

처음 개발 공부를 할 때 props드릴링을 개선하기 위해 전역상태를 사용한다고 배웠습니다.

개인적으로 전역상태 라이브러리로 recoil을 사용했는데, 전역상태를 많이 만드는 것은 좋아하지 않습니다.

첫 번째 이유는 const [state, setState] = useRecoilState(globalRecoilName);

이렇게 가져오는 state명을 컴포넌트마다 바꿔서 사용할 수 있으므로 해당 컴포넌트에서 사용하는 전역상태가 어떤 상태인지 파악하려면

globalRecoilName이라는 atom변수명을 기억해야 하기 때문에 상태가 기능에 콤팩트하게 결합되었다고 느끼지 않습니다.

두 번째는 컴포넌트를 작게, 재사용성 있게 작성한다는 전제하에.. 특정한 기능을 하는 작은 트리에서만 공유하는 상태가 대부분인데, 

이 상태를 공유하기 위해 프로젝트 앱 전체에서 공유되는 전역상태를 만들어버리면 마찬가지로 기능별로 한눈에 보기 힘들게 합니다.

즉, 전역상태라이브러리를 사용하는 이유는 진짜 전역(이름도 거창한 Global)에서

특정 데이터를 꼭 사용하는 경우가 필요할 때 사용해야 한다고 생각합니다.

이러한 이유로 전역상태 라이브러리를 사용할 때 신중하게 결정하는 편입니다.

 

이번에 개발한 필터기능은 전역으로 관리할 필요는 없었고 특정 모듈에서만 공유하는 상태임이 분명했습니다.

그러다 보니 filter상태를 여러 바텀시트에 반복적으로 내려주는 불편함이 생겼습니다.

props드릴링이 그렇게 깊지는 않지만(기능이 추가되면 깊어질 가능성이 충분해 보이네요)

반복되는 props전달이 보기 안 좋습니다.

 

해결책 3. contextAPI를 사용하여 내가 원하는 트리에서 공유되는 상태를 만든다.

리액트 공식문서

context 또한 전역적으로 데이터를 공유하기 위해 고안된 방법이라고 합니다. 

단, 리코일과 다르게 공유하고자 하는 컴포넌트 트리를 개발자가 직접 선언적으로 지정할 수 있었습니다.

저는 이 차이가 중요하다고 생각합니다. 어떤 의도를 가지고 어떤 트리까지 공유되는 상태를 만들었는지 표현할 수 있으니까요.

그래서 저는 필터 모듈에서 공유할 수 있는 context를 만들어서 위의 불편함을 해결하고자 했습니다.

사용법은 공식문서를 참고해 주세요.(ContextApi 사용법)

 

Context Provider를 만들고 여기서 위에 만들었던 useReducer의 상태와 dispatch를 공급하도록 할게요.

이 또한 공식문서에서 언급된 방법입니다.

Scaling Up with Reducer and Context

import { createContext, useContext, useReducer } from 'react';
import { toggleState } from 'utils/CommonFunctions';

const FilterContext = createContext(null);
const FilterDispatchContext = createContext(null);
const ModalContext = createContext(null);
const ModalDispatchContext = createContext(null);

export const FilterProvider = ({ children, initFilter, initModal }) => {
  const [filter, dispatch] = useReducer(filterReducer, initFilter);
  const [modal, modalDispatch] = useReducer(modalReducer, initModal);

  function filterReducer(state, action) {
    // 반복되는 로직 작성
  }
  function modalReducer(state, action) {
    // 반복되는 로직 작성
    }
  }

  return (
    <ModalContext.Provider value={modal}>
      <ModalDispatchContext.Provider value={modalDispatch}>
        <FilterContext.Provider value={filter}>
          <FilterDispatchContext.Provider value={dispatch}>
            {children}
          </FilterDispatchContext.Provider>
        </FilterContext.Provider>
      </ModalDispatchContext.Provider>
    </ModalContext.Provider>
  );
};

export function useFilter() {
  return useContext(FilterContext);
}

export function useFilterDispatch() {
  return useContext(FilterDispatchContext);
}

export function useModal() {
  return useContext(ModalContext);
}

export function useModalDispatch() {
  return useContext(ModalDispatchContext);
}

 

이 FilterProvider라는 컴포넌트의 children은 createContext로 만든 context의 Provider를 받습니다.

즉, FilterProvider 아래의 트리는 context로 공급받은 상태에 접근할 수 있습니다. 

저는 바텀시트의 상태도 관리해야 하기 때문에 modal 상태도 추가했습니다.

이제 Provider에서 dispatch, filter, modal 상태를 가져올 수 있기 때문에 필요한 props를 챙겨서 찔러주지 않아도 됩니다.

최종적으로 컴포넌트는 아래와 같이 개선됩니다.(실제 코드가 아닌 개선점을 표현하기 위한 예제입니다)

const ParentComponent = () => {
  return (
    <FilterProvider>
      <ChildrenFilter />
    </FilterProvider>
  );
};

const ChildrenFilter = () => {
  const dispatch = useFilterDispatch();

  const handleClick = (id, item) => {
    dispatch({ key: 'product', payload: item, type: 'toggle' });
  };

  return (
    <div onClick={handleClick}>
      <TotalSelectSheet />
      <LocationSelectSheet />
      <ProductSelectSheet />
      <OriginSelectSheet />
      <RaisedSelectSheet />
    </div>
  );
};

 

Reducer와 Context의 조합으로 반복되는 props전달문제와 반복되는 setState로직 작성 문제로부터 해방되었습니다!

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2025/05   »
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 31
글 보관함