React를 활용하여 간단한 아코디언 메뉴바 만들기

김현재·2021년 10월 17일
5

기업협업 project

목록 보기
4/8

2중 아코디언 메뉴를 만들었는데 생각보다 어려웠다..!
이 어려운 것을 어떻게 해쳐나갔는지 회고해보자.

목표 구현사항

  • 1단 메뉴를 눌렀을 때 2단 메뉴가 펼쳐진다 (그 외 다른 메뉴들은 1단만 보이게 한다)
  • 1단 메뉴 및 2단 메뉴를 클릭했을 때 글자를 bold처리 되게 한다.

까다로웠던 이유

  1. 1단 메뉴용 클릭이벤트와 2단 메뉴용 클릭이벤트를 어떻게 분리해야할지 감이 안잡혔다.
    1단 메뉴에 클릭 이벤트를 준 다음에 2단 메뉴에도 클릭 이벤트를 주니까 이벤트 버블링이 일어나서..1단 클릭 이벤트가 반응하여 1단 메뉴가 닫혀버려 2단을 클릭할 수 없었다.

  2. 1단 메뉴의 클릭 이벤트가 각각 메뉴명에 적용되는게 아닌, 동시에 적용되어 하나가 켜졌을 때 하나가 꺼지게 하는 방법을 찾기 어려웠다.
    map을 돌린 상태에서 1단 메뉴에 onclick이벤트를 주니, 각 메뉴별로 이벤트가 별도로 들어가져서, TAMS를 클릭한 다음에 Jupiter를 클릭하니 두 메뉴가 모두 펼쳐지는 상황이 발생하였다..state사용을 해야되는 것 까지는 인지했으나 state를 무엇을 관리할지 감이 안잡혔다.

해결 방안

클릭이벤트가 발생해야 할 요소 index를 state로 관리하니 간단하게 해결되었다!

1. index를 활용하여 1단 메뉴의 클릭이벤트가 동시에 반영되도록 함

const AccordianMenu = () => {

  const MENU_LIST = [
    { title: 'TAMS', list: ['Users', 'Wallets'] },
    { title: 'Jupiter', list: ['Create', 'Read', 'Update', 'Delete'] },
  ];

  const [activeIndex, setActiveIndex] = useState();

  return (
    <Nav>
      <TitleWrapper>
        <Title>DigiFinance</Title>
      </TitleWrapper>
      <Ul>
        {MENU_LIST.map((item, idx) => {
          const active = idx === activeIndex ? 'active' : '';
          
          return (
            // 1단 메뉴 부분 
            // ListItem 하나 하나가 <li>이다
            <ListItem
              title={item.title}
              idx={idx}
              list={item.list}
              active={active}
              activeIndex={activeIndex}
              setActiveIndex={setActiveIndex}
            />
          );
        })}
      </Ul>
    </Nav>
  );
};

우선 확장성을 고려하여 메뉴리스트는 별도 Array로 관리되도록 하였다.

1단 메뉴의 클릭 이벤트는 activeIndex라는 state를 만들어서 관리되도록 하였다.
전체 메뉴를 map 돌리면서 발생된 index를 참고하는 state로, 클릭된 하나의 ListItem에 대한 index를 저장한다.

active라는 변수는 activeIndex와 인덱스 번호가 일치하는 ListItem컴포넌트를 찾아내 그곳의 className으로 활용된다.
이 변수를 활용하여 클릭된 ListItem 컴포넌트에만 focus되었다는 서식을 줄 예정이다.

1단 메뉴와 2단 메뉴의 click event를 index를 활용하여 분리함

const ListItem = ({
  title,
  list,
  active,
  activeIndex,
  setActiveIndex,
  idx,
}) => {
  
  const history = useHistory();
  const [clickedIdx, setClickedIdx] = useState();

  // 1단 메뉴 클릭 이벤트 처리 함수
  // 위에서 언급한 1단 메뉴 클릭 시 activeIndex라는 state에 해당 인덱스를 저장해준다
  const handleClick = () => {
    setActiveIndex(idx);
    setClickedIdx(null);
    history.push(`/${title}`);
  };

  // 2단 메뉴 클릭 이벤트 처리 함수
  const handleLink = (e, idx) => {
    setClickedIdx(idx);
    history.push({
      pathname: `/${title}`,
      state: {
        clicked: idx,
      },
    });
  };

  return (
    <Li>
      // 상위 컴포넌트에서 받아온 active 변수를 className으로 넘겨준다
      // 이 때 `active`라는 변수가 활성화되면 이에 대응하는 스타일링을 처리해준다
      <AccodianWrapper className={active}>
        <FirstMenu onClick={handleClick}>
          <IconWrapper>
            <RiDashboardLine />
          </IconWrapper>
          <Menu>{title}</Menu>
        </FirstMenu>
        <SecondMenu className={idx === activeIndex ? '' : 'closed'}>
          {list?.map((menu, idx) => (
            <li
              onClick={e => handleLink(e, idx)}
              className={clickedIdx === idx ? 'strong' : ''}
            >
              {menu}
            </li>
          ))}
        </SecondMenu>
      </AccodianWrapper>
    </Li>
  );
};

2단 메뉴도 1단 메뉴와 동일하게 클릭한 index를 state에 저장하여 추적하도록 하였다.
그래서 map을 돌린 요소 중 state에 저장된 index에 해당하는 요소만 꼭 찝어내어 스타일링과 비즈니스로직이 진행되도록 하였다.

함수명과 state를 1단 메뉴와 분리하여 각자 개별적으로 클릭 이벤트가 일어나도록 하였다.

고민의 회고 - index좀 써라~!

index를 사용하는 것이 적절하였는데 계속 문자열을 활용하여 state를 처리하려고 하다보니까 더 어렵게 문제를 꼬아버린 것 같다.
특히 map을 돌리는 상황에서는 index를 활용하는것이 반환된 여러 요소 중 특정 요소에 대한 event처리에 훨씬 용이하다는 것을 잊지 말 것!
(아니 map 돌릴 때 index로 key를 지정하지 말라고 하니까 index 사용을 너무 소극적으로 하게 되었다..그래도 쓸 때는 써야지!)

참고자료

profile
쉽게만 살아가면 재미없어 빙고!

0개의 댓글