• [글또 10기 - 6] 백엔드 개발자가 처음해보는 리액트 프론트엔드 개발 - 사이드바 하위메뉴 기능 분석
    개발공부/무작정만들어보자 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 // 값유지
    ]

     

     

     

     

     

     

     

     

     

     

     

    반응형

    댓글

Designed by Tistory.