createContext 를 통한 하위 컴포넌트들의 상태관리

김재욱·2023년 6월 3일
0

react

목록 보기
1/5


위 사진의 icon들은 사용자가 클릭 후 홈으로 가기 버튼을 누르면 사용자가 클릭한 결과가 사용 만족도로 서버에 전송되어야 했다. 하지만 이 아이콘들은 각각의 compenent로 독립적으로 관리되고 있었고 여러 가지 아이콘이 클릭되는 버그가 존재했다. 따라서 이 버그를 해결하기 위해서는 5개의 아이콘을 그룹화하여 같이 상태관리를 하여 최대 1개 혹은 0개의 아이콘이 클릭되게 수정해야 했다.

첫 번째 시도

const FeedbackResultHelpfulnessModal = ({ title, visible, close }: FeedbackHelpModalProps) => {
 const [iconIndex, setIconIndex] = useState([false, false, false, false, false]);
 const handleIconClick = (index: number) => {
   let updatedIndex = [...iconIndex];
   if( updatedIndex[index] == true ){  updatedIndex[index]= false; }
   else{ 
     updatedIndex = Array(5).fill(false);
     updatedIndex[index]=true;
   }
   setIconIndex(updatedIndex);
 }
 return (
   ...
     <IconContainer as={'main'} flex={'rowCenter'} >
       <CircleIconChip name={'faceWorst'} width={20} height={20} viewBox={'0 0 20 20'} index={0} handleclick={handleIconClick}iconList={iconIndex} />
       <CircleIconChip name={'faceBad'} width={20} height={20} viewBox={'0 0 20 20'} index={1} handleclick={handleIconClick}iconList={iconIndex} />
       <CircleIconChip name={'faceNormal'} width={20} height={20} viewBox={'0 0 20 20'} index={2} handleclick{handleIconClick} iconList={iconIndex} />
       <CircleIconChip name={'faceGood'} width={20} height={20} viewBox={'0 0 20 20'} index={3} handleclick={handleIconClick}iconList={iconIndex} />
       <CircleIconChip name={'faceBest'} width={20} height={20} viewBox={'0 0 20 20'} index={4} handleclick={handleIconClick}iconList={iconIndex} />
     </IconContainer>
   ...
 );
};

이 코드가 나의 첫 시도였다. 아이콘이 5개이므로 길이가 5인 배열을 통해 아이콘들의 클릭 여부를 확인하고 2개 이상의 아이콘이 불이 켜지려고 하는 순간 마지막으로 눌린 아이콘만 불이 켜지게 하는 코드였다. 아이콘이 켜지면 true, 꺼지면 false이고 두 개 이상이 true가 되려고 하면 마지막에 누른 값만 true로 변하는 구조이다.

사실 이 코드로도 내가 원하던 기능은 구현이 끝났다. 그리고 테스트 결과로는 별다른 error가 발견되지는 않았지만, 더 많은 정보를 포함하여 다양한 기능을 관리해야 하는 가능성, 더 많은 아이콘 (ex. 100개의 아이콘을 관리해야 할 경우) 등을 생각해 보았을 때 5개 아이콘의 true or false 값만 관리해 주는 코드가 아닌 여러 가지 상태를 관리할 수 있고, 많은 아이콘들을 관리할 수 있는 코드를 작성해 보기로 했다.

두 번째 시도

https://www.daleseo.com/react-checkboxes/ 이 블로그를 참고하여 코드 리펙터링을 진행하였다

//CircleIconContext.tsx
import React, { Dispatch, ReactNode, SetStateAction } from "react";
import { createContext } from "react";

type CircleIconContextProps = {
    isChecked: (value?: string) => boolean;
    toggleValue: (checked?: string, value?: string) => void;
};

export const CircleIconContext = createContext<CircleIconContextProps | null>(null);

createContext를 활용하여 하위 컴포넌트에 원하는 값을 전달할 수 있기에 이 기능을 활용하여 icon들을 감싸줄 group에서 어떤 아이콘이 checked 되었는지를 관리하여 보자.

//CircleIconGroup.tsx
export const CircleIconGroup = ({ children, values, onChange }: CircleIconGroupProps) => {
    const isChecked = (value: any) => values === value;
    const toggleValue = (value: any) => {
        if(value===values){
            onChange("");
        }
        else{
            onChange(value);
        }
    }
    return(
        <CircleIconContext.Provider value={{ isChecked, toggleValue }}>
            {children}
        </CircleIconContext.Provider>
    )
};

icon들을 감싸줄 CircleIconGroup를 위에서 선언한 CircleIconContext로 감싸서 CircleIconGroup의 하위 컴포넌트들을 ischecked와 toggleValue를 통해 관리해 보자.

//CircleIconChip.tsx
const CircleIconChip = (props: CircleIconChipProps) => {
  const context = React.useContext(CircleIconContext);
  const [isToggled, setIsToggled] = useState<boolean>(false);
  ...
  const { isChecked, toggleValue } = context;

  return (
    <S.Root>
      <S.Wrapper
        flex={'rowCenter'}
        isToggled={isChecked(props.name)}
        onClick={() => toggleValue(props.name)} >
        <SVGIcon {...props}></SVGIcon>
      </S.Wrapper>
    </S.Root>
    );
};

하위 컴포넌트인 CircleIconChip에서는 CircleIconContext를 통해 받은 ischecked와 togglevalue를 통해 본인의 isToggled 값과 onclick함수를 설정해 줄 수 있다. 이렇게 코드를 작성하면 장점은 하나의 아이콘의 isToggled값이 바뀔 때마다 circleIconGroup이 다시 랜더링 되어 하위 컴포넌트들도 같이 랜더링 되면서 여러 개의 아이콘이 켜질 일이 없다.

//FeedbackResultHelpfulnessModal.tsx
//아까 사진에 해당하는 modal이다.
const FeedbackResultHelpfulnessModal = ({ title, visible, close }: FeedbackHelpModalProps) => {
    const [iconIndex, setIconIndex] = useState<string>("");

    ...

    return(
        ...

        <CircleIconGroup values={iconIndex} onChange={setIconIndex} >
            <CircleIconChip name={'faceWorst'} width={20} height={20} viewBox={'0 0 20 20'} />
            <CircleIconChip name={'faceBad'} width={20} height={20} viewBox={'0 0 20 20'} />
            <CircleIconChip name={'faceNormal'} width={20} height={20} viewBox={'0 0 20 20'} />
            <CircleIconChip name={'faceGood'} width={20} height={20} viewBox={'0 0 20 20'} />
            <CircleIconChip name={'faceBest'} width={20} height={20} viewBox={'0 0 20 20'} />
        </CircleIconGroup>
        ...
    )

이 아이콘들을 사용하는 모달에서 다음과 같이 코드를 작성하면 어떠한 CircleIconChip을 클릭하게 되면 setIconIndex가 실행되면서 iconIndex값이 해당 CircleIconChip의 name값으로 바뀌게 된다. 이 iconIndex에 따라 각각의 icon들이 본인이 체크되었는지를 확인할 수 있고, 이를 통해 배경색을 달리 하던가 하는 시각적인 효과도 구현할 수 있었다.

느낀 점

다음과 같이 코드를 작성하고 나니, 나와 같이 작업하신 분이 왜 저렇게 코드를 리펙터링 하자고 하셨는지 알 수 있었다. 예를 들어 저 modal이 아닌 다른 modal에서 회원가입과 관련된 모달인 경우 반드시 체크해야 하는 박스와 그러지 않아도 되는 박스가 있을 수 있다. 내가 처음 쓴 코드로는 그런 기능 구현이 불가능하거나, 거기서만 사용할 수 있는 코드를 작성하여 유지보수가 어려울 수 있다. 하지만 지금처럼 코드를 작성하고 나니 다른 기능의 추가도 가능할 거 같고 더 많은 아이콘들이 생기더라도 내가 할 일은 iconGroup안에 icon을 추가하는 일 말고는 없었다!

코드를 작성할 때 원하는 기능을 구현하는 것이 중요한 것은 맞지만, 컴포넌트의 자식 부모 관계를 고려하며 어떠한 기능을 구현하기 위해 책임을 져야 하는 컴포넌트에서 해당 기능을 구현해야 하고, 기왕이면 유지보수, 기능확장이 용이하게 작성하는 것이 좋다!

profile
초보 개발자의 우당탕탕 코딩일기

0개의 댓글