개발공부/무작정만들어보자

[글또 10기 - 6] 백엔드 개발자가 처음해보는 리액트 프론트엔드 개발 - 사이드바 하위메뉴 기능 분석

원석💎-dev 2025. 2. 16. 18:34
반응형

하위 메뉴 기능을 개발하였고, 아래와 같이 사이드바 코드가 추가되었다. 이제 코드를 하나씩 분석해보자

// ./component/Sidebar.tsx

'use client'; // 클라이언트 전용 훅(useState)을 사용해야 할 경우 SSR이 아닌 CSR로 동작해야 한다.

import { useState } from 'react';

interface MenuItem {
  label: string;
  href?: string;
  subMenu?: MenuItem[];
}

const menuItems: MenuItem[] = [
  { href: '/pages/about', label: 'onestone' },
  { href: '#', label: '스프링부트' },
  { label: '스프링배치', 
    href: '#',
    subMenu: [
      { label: '5.0.2', href: '/pages/spring_batch/5.0.2' },
    ],
  },
  { href: '#', label: '코틀린' },
  { href: '#', label: '그레이들' },
  { href: '#', label: '깃헙' },
  { href: '#', label: '스프링 카프카' },
  { href: '/pages/glossary', label: '용어 사전 및 규칙' },
];

const Sidebar: React.FC = () => {
  const [openMenus, setOpenMenus] = useState<{ [key: string]: boolean }>({}); // 상태관리

  const toggleMenu = (label: string) => {
    setOpenMenus((prev) => ({
      ...prev,
      [label]: !prev[label],
    }));
  };

  const renderMenu = (items: MenuItem[]) => {
    return (
      <ul className={`${styles.menuList}`}>
        {items.map((item) => (
          <li key={item.label} className={styles.menuItem}>
            {item.subMenu ? (
              <>
                <button
                  onClick={() => toggleMenu(item.label)}
                  className={styles.menuButton}
                  aria-expanded={openMenus[item.label] || false}
                >
                  {item.label}
                </button>
                {openMenus[item.label] && renderMenu(item.subMenu)}
              </>
            ) : (
              <Link href={item.href || '#'}>{item.label}</Link>
            )}
          </li>
        ))}
      </ul>
    );
  };

  return <div className={styles.sidebar}>
    {renderMenu(menuItems)}
    </div>;
};

export default Sidebar;

 

 

 

 

 

1. 상태 관리 Hook - useState

useState는 리액트 16.8부터 추가된 컴포넌트의 상태를 관리하는 Hook이다. useState는 다음과 같이 사용된다.

const [변수, 변수를_변경할_함수] = useState<제네릭>(기본값);

 

 

간단한 예시로 다음과 같이 만들 수 있다.

'use client';

import React, { useState } from 'react';

const Sidebar: React.FC = () => {
  // const [a, b]: [number, React.Dispatch<React.SetStateAction<number>>] = useState<number>(0);
  const [a, b] = useState<number>(0);

  return (
    <>
      <h1>{ a }</h1>
      <button onClick={() => {b(a + 1);}}>증가</button>
    </>
  );
}

 

a는 숫자를 담을 변수고 b는 숫자를 변경할 함수다. 네이밍을 개발자 마음데로 변경 할 수도 있다는 것을 보여주기 위해 a, b로 표기했다.

 

 

 

 

 

아래 코드에서는 openMenus 변수를 setOpenMenus 함수가 변경하는 것으로 이해하면 된다. 변수의 기본값음 객체 {} 이다.

const [openMenus, setOpenMenus] = useState<{ [key: string]: boolean }>({});

 

변수의 타입은 제네릭으로 선언되어 있다. 타입스크립트의 인덱스 시그니처(Index Signature)를 이해해야 분석할 수 있다.

 

 

 

 

 

2. 인덱스 시그니처 - { [key: string]: boolean }

인덱스 시그니처는 { [key : T] : U } 형식이다. 위 코드에서는 { [key: string]: boolean } 로 구현되어 있다.

 

 key는 객체의 키(key)를 의미한다. key: string이므로 key는 문자열 이다. 예를들어, "a", "b", "c" 일 수 있다. value는 객체의 값을 의미한다. [key: string]: boolean이므로 value는 boolean이다. 예를들어 true, false, true이다. 아래 예시 코드를 실행해 보면 이해가 가능할 것이다.

'use client';

const test: { [key: string]: boolean } = {
  "a": true,
  "b": false,
  "c": true
};

const Sidebar: React.FC = () => {
  console.log(`a: ${test["a"]}`);
  console.log(`b: ${test["b"]}`);
  console.log(`c: ${test["c"]}`);
  
  return <div className={styles.sidebar}></div>
}

결과 콘솔 값을 보면, 값이 두 번씩 찍히는데, React.StrictMode가 활성화된 개발 환경에서 리액트가 일부 컴포넌트를 두 번 렌더링하기 때문이다. 개발 모드에서 컴포넌트의 예상치 못한 부작용을 감지하기 위해 추가적으로 렌더링을 두 번 수행하는 기능이므로, 프로덕션 환경에서는 작동하지 않는다.

 

npm run dev가 아니라 npm run build 후 npm run start를 하면 한 번만 찍히는 것을 확인할 수 있다.

 

 

 

 

 

3. 스프레드 연산자(...) - toggleMenu

toggleMenu는 화살표 함수 방식으로 작성되었다. return 값은 없고, openMenu의 값만 변경해준다. 코드를 분석하기 위해서는 스프레드 연산자(...)를 이해해야 한다.

  const toggleMenu = (label: string) => {
    setOpenMenus((prev) => ({
      ...prev,
      [label]: !prev[label],
    }));
  };

 

...prev는 이전 존재했던 객체(openMenus)의 키-값(value)를 유지한다.

[label]: !prev[label], 로 이전 객체(openMenus)의 특정 키를 찾아 값을 반전시켜준다.

 

예를들어, openMenus의 값 세팅이 다음과 같고, '스프링부트' 메뉴를 클릭한다. 그러면, openMenus의 값을 유지(...prev)하고, 객체의 키인 '스프링부트'를 찾아 값인 false에서 true로 변환한다.

// 값 실행 전
const openMenus = [
   'onestone': false, 
   '스프링부트': false, 
   '스프링배치': false, 
   '코틀린': false
]
// 값 실행 후
const openMenus = [
   'onestone': false, // 값유지
   '스프링부트': true, // 변경됨.
   '스프링배치': false, // 값유지
   '코틀린': false // 값유지
]

 

 

 

 

 

 

 

 

 

 

 

반응형