• [글또 10기 - 5] 백엔드 개발자가 처음해보는 리액트 프론트엔드 개발 - 사이드바 하위메뉴 기능 개발
    개발공부/무작정만들어보자 2025. 1. 19. 15:35
    반응형

     

     4편에서 사이드바 메뉴와 컨턴츠 페이지를 개발했다. 이번에는 사이드바의 하위메뉴 기능을 구현하려고 한다. 지금까지의 폴더 구조는 아래와 같다.

    Root 폴더
    +--- .next
    +--- public
    +--- src
          \---app
               \---module
                     \---Sidebar.module.css  # 사이드바 css   
                \---pages
                	  \---glossary
                              \--- page.tsx # 용어사전 및 규칙 페이지
               +--- Sidebar.tsx # 사이드바 컴포넌트
               +---globals.css  # 글로벌 css
               +---layout.tsx   # 전체 레이아웃
               \---page.tsx     # 첫 번째 페이지
    +--- .eslintrc.json
    +--- .gitignore
    +--- next-env.d.ts
    +--- next.config.js
    +--- package-lock.json
    +--- package.json           # 라이브러리 버전(react: ^18,react-dom: ^18, next: 13.5.7)
    +--- postcss.config.js
    +--- README.md
    +--- tailwind.config.ts 
    \--- tsconfig.json          # alias 파일

     

     

     

     

    사이드바 컴포넌트 리펙토링

    현재 사이드바 컴포넌트의 구조는 다음과 같다.

    // ./component/Sidebar.tsx
    
    import Link from 'next/link';
    import styles from './Sidebar.module.css';
    
    const Sidebar: React.FC = () => {
      return (
        <div className={styles.sidebar}>
          <ul>
            <li>
              <Link href="#">onestone</Link>
            </li>
            <li>
              <Link href="#">스프링부트</Link>
            </li>
            <li>
              <Link href="#">스프링배치</Link>
            </li>
            <li>
              <Link href="#">코틀린</Link>
            </li>
            <li>
              <Link href="#">그레이들</Link>
            </li>
            <li>
              <Link href="#">깃헙</Link>
            </li>
            <li>
              <Link href="#">스프링 카프카</Link>
            </li>
            <li>
              <Link href="/pages/glossary">용어 사전 및 규칙</Link>
            </li>
          </ul>
        </div>
      );
    };

     

     

    저번 글에서 작성한 기획서를 보면 메뉴를 누르면 하위 메뉴가 나오고 하위 메뉴의 하위 메뉴를 구현해야한다. "메뉴 > 하위메뉴 > 하위메 뉴" 이렇게 처리하기 위해 하나씩 추가해주던 사이드바의 메뉴를 변수로 선언하고, 동적으로 받을 수 있게 페이지를 변경하려고 한다.

     

    [글또 10기 - 2] 백엔드 개발자가 처음해보는 리액트 프론트엔드 개발 - 기획과 설계

     

     

     

    아래와 같이 인터페이스와 그 인터페이스를 상속한 menuItems 변수를 만들어 추가한다.

    // ./component/Sidebar.tsx
    
    import Link from 'next/link';
    import styles from './Sidebar.module.css';
    
    interface MenuItem {
      label: string;
      href: string;
    }
    
    const menuItems: MenuItem[] = [
      { href: '#', label: 'onestone' },
      { href: '#', label: '스프링부트' },
      { href: '#', label: '스프링배치' },
      { href: '#', label: '코틀린' },
      { href: '#', label: '그레이들' },
      { href: '#', label: '깃헙' },
      { href: '#', label: '스프링 카프카' },
      { href: '/pages/glossary', label: '용어 사전 및 규칙' },
    ];
    
    ...

     

     

    하드 코딩된 메뉴리스트를 제거하고, menuItems를 받을 수 있게 수정해준다. html DOM tree 구조는 <div><ul><li><Link> 그대로 유지한다.

    import Link from 'next/link';
    import styles from './module/Sidebar.module.css';
    
    interface MenuItem {
      label: string;
      href: string;
    }
    
    const menuItems: MenuItem[] =  [
      { href: '#', label: 'onestone' },
      { href: '#', label: '스프링부트' },
      { href: '#', label: '스프링배치' },
      { href: '#', label: '코틀린' },
      { href: '#', label: '그레이들' },
      { href: '#', label: '깃헙' },
      { href: '#', label: '스프링 카프카' },
      { href: '/pages/glossary', label: '용어 사전 및 규칙' },
    ];
    
    const Sidebar: React.FC = () => {
      return (
        <div className={styles.sidebar}>
          <ul>
            {menuItems.map((item) => (
              <li key={item.href}>
                <Link href={item.href}>{item.label}</Link>
              </li>
            ))}
          </ul>
        </div>
      );
    };
    
    export default Sidebar;

     

    잘 표현되는지 페이지 이동은 잘 되는지까지 확인했다.

    더보기

     타입스크립트에도 forEach가 있는 것으로 아는데 왜 map을 사용해야하는지 의문이 들었다. 처음 헷갈렸던 이유는 백엔드 코드에서 출력을 위해 반복문을 사용할 경우 map을 사용하지 않기 때문이다. foreach는 반환하는 값이 없으므로 로그와 같은 출력을 위해 사용한다. 하지만, 프론트엔드 코드에서는 출력을 HTML 렌더링과 콘솔 출력으로 나누어 생각해야 한다. 프론트에서는 HTML렌더링을을 위해 미리 HTML DOM Tree를 만들어야 한다. 그러므로, map으로 HTML코드를 반환하고, 생성된 전체 코드를 렌더링한다.

     

     

     

     

    사이드바 컴포넌트 서브메뉴 기능 추가

    서브메뉴 기능을 추가하기 위해 인터페이스를 수정했다. href는 nullable하게 바꾸고, subMenu 목록을 추가했다.

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

     

     

     menuItems도 변경된 인터페이스에 맞게 수정한다.

    ...
    
    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: '용어 사전 및 규칙' },
    ];
    
    ...

     

     

     

    토글 메뉴로 UI를 개선한다. subMenu가 존재할 경우 버튼으로, subMenu가 없을 경우 Link로 메뉴를 구성한다. 

    'use client'; // 클라이언트 전용 훅(useState)을 사용해야 할 경우 SSR이 아닌 CSR로 동작해야 한다.
    
    import { useState } from 'react';
    
    ...
    
    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;

     

     

    스프링배치에만 subMenu를 넣어줬기 때문에 스프링배치 메뉴만 버튼으로 변경됐다. 

    반응형

    댓글

Designed by Tistory.