Logo

React로 사이드 네비게이션 UI 구현하기

사이트 네비게이션(site navigation)은 웹사이트에서 사용자가 효과적으로 페이지 사이를 이동을 할 수 있도록 도와주는 매우 흔하면서도 중요한 UI 입니다. 이번 포스팅에서는 React와 Styled Components를 이용하여 아래와 같은 다단계 사이드 네비게이션 UI를 한 번 구현해도록 하겠습니다.

컴파운트 컴포넌트

HTML에는 부모없이는 단독으로 쓰이지 않는 요소들이 있습니다. 대표적인 예로 <option> 요소는 항상 <select> 요소 안에서 사용되며, <li> 요소는 항상 <ul>이나 <ol> 요소 안에서 사용됩니다.

이렇게 부모 자식으로 항상 함께 사용되어야 하는 컴포넌트들을 구조화할 때 React에서는 컴파운트(compound) 컴포넌트로 구현하는 경우가 많습니다. 네비게이션 UI의 경우 흔히 다음과 같이 HTML의 <nav><ul>, <li>, <a>, <hr> 요소로 마크업이 되는데요.

<nav>
  <ul>
    <li>
      <a href="/">Home</a>
    </li>
    <li>
      <a href="/about">About</a>
    </li>
    <li role="presentation">
      <a>Coming Soon</a>
    </li>
    <hr role="presentation" />
    <li>
      <a href="/back/python">Backend</a>
      <ul>
        <li>
          <a href="/back/python">Python</a>
        </li>
        <li>
          <a href="/back/java">Java</a>
        </li>
      </ul>
    </li>
    <li>
      <a href="/front/html">Frontend</a>
      <ul>
        <li>
          <a href="/front/html">HTML</a>
        </li>
        <li>
          <a href="/front/css">CSS</a>
        </li>
        <li>
          <a href="/front/js/react">JavaScript</a>
          <ul>
            <li>
              <a href="/front/js/react" aria-current="page">React</a>
            </li>
            <li>
              <a href="/front/js/vue">Vue</a>
            </li>
          </ul>
        </li>
      </ul>
    </li>
    <hr role="presentation" />
    <li>
      <a href="/help">Help</a>
    </li>
  </ul>
</nav>

이를 React의 JSX로 나타내보면 다음과 같이 <Nav> 컴포넌트 하나로 묶어서 깔끔하게 표현할 수가 있습니다.

/src/SideNav.jsx
import Nav from "./Nav";

function isActive(path) {
  return window.location.pathname.startsWith(path);
}

function SideNav() {
  return (
    <Nav>
      <Nav.List>
        <Nav.Item>
          <Nav.Link to="/" active={isActive("/")}>
            Home
          </Nav.Link>
        </Nav.Item>
        <Nav.Item>
          <Nav.Link to="/about" active={isActive("/about")}>
            About
          </Nav.Link>
        </Nav.Item>
        <Nav.Item disabled>
          <Nav.Link>Coming Soon</Nav.Link>
        </Nav.Item>

        <Nav.Separator />

        <Nav.Item>
          <Nav.Link to="/back/python" active={isActive("/back")}>
            Backend
          </Nav.Link>
          <Nav.List expanded={isActive("/back")}>
            <Nav.Item>
              <Nav.Link to="/back/python" active={isActive("/back/python")}>
                Python
              </Nav.Link>
            </Nav.Item>
            <Nav.Item>
              <Nav.Link to="/back/java" active={isActive("/back/java")}>
                Java
              </Nav.Link>
            </Nav.Item>
          </Nav.List>
        </Nav.Item>

        <Nav.Item>
          <Nav.Link to="/front/html" active={isActive("/front")}>
            Frontend
          </Nav.Link>
          <Nav.List expanded={isActive("/front")}>
            <Nav.Item>
              <Nav.Link to="/front/html" active={isActive("/front/html")}>
                HTML
              </Nav.Link>
            </Nav.Item>
            <Nav.Item>
              <Nav.Link to="/front/css" active={isActive("/front/css")}>
                CSS
              </Nav.Link>
            </Nav.Item>
            <Nav.Item>
              <Nav.Link to="/front/js/react" active={isActive("/front/js")}>
                JavaScript
              </Nav.Link>
              <Nav.List expanded={isActive("/front/js")}>
                <Nav.Item>
                  <Nav.Link
                    to="/front/js/react"
                    active={isActive("/front/js/react")}
                  >
                    React
                  </Nav.Link>
                </Nav.Item>
                <Nav.Item>
                  <Nav.Link
                    to="/front/js/vue"
                    active={isActive("/front/js/vue")}
                  >
                    Vue
                  </Nav.Link>
                </Nav.Item>
              </Nav.List>
            </Nav.Item>
          </Nav.List>
        </Nav.Item>

        <Nav.Separator />

        <Nav.Item>
          <Nav.Link to="/help" active={isActive("/help")}>
            Help
          </Nav.Link>
        </Nav.Item>
      </Nav.List>
    </Nav>
  );
}

export default SideNav;

지금부터 위와 같은 다단계 사이드 네비게이션 UI를 구현하는 컴파운트 컴포넌트를 작성해보겠습니다.

먼저 전체 네비게이션을 감싸는 <Nav/> 컴포넌트는 HTML의 <nav> 요소를 기반으로 작성합니다. 화면 촤측에 위치할 것이기 때문에 최소 너비와 우측 내부 여백을 스타일하겠습니다.

children prop으로 넘어온 자식 컴포넌트들을 그대로 랜더링해주면 됩니다.

/src/Nav/Nav.jsx
import styled from "styled-components";

const Navigation = styled.nav`
  min-width: 200px;
  padding-right: 20px;
`;

function Nav({ children }) {
  return <Navigation>{children}</Navigation>;
}

export default Nav;

네비게이션 리스트를 나타내는 <NavList/> 컴포넌트는 HTML의 <ul> 요소를 기반으로 작성합니다. 다단계 네비게이션 UI를 구현할 때는 들여쓰기를 활용하여 각 링크가 몇 단계에 있는지 시각적인 효과를 주는 것이 중요합니다.

expanded prop은 시각적으로 리스트을 열어놓을지 닫아놓을지 여부를 결정합니다. 리스트 하위에 현재 페이지에 해당하는 링크가 있을 때만 리스트가 열려있도록 스타일하겠습니다.

/src/Nav/NavList.jsx
import React from "react";
import styled from "styled-components";

const List = styled.ul`
  display: ${(p) => (p.expanded ? "block" : "none")};
  margin: 0;
  padding: 0;
  padding-left: 20px;
  list-style: none;
`;

function NavList({ children, expanded = true }) {
  return <List expanded={expanded}>{children}</List>;
}

export default NavList;

네비게이션 아이템을 나타내는 <NavItem/> 컴포넌트는 HTML의 <li> 요소를 기반으로 작성합니다.

disabled prop은 아이템의 비활성화 여부를 결정합니다. 하위에 비활성화된 링크가 있는 경우에는 리스트에서 해당 아이템이 제외될 수 있도록 role 속성을 presentation으로 지정합니다. 시각적인 효과는 없으며 웹 접근성(accessibility) 측면에서 권장되는 부분입니다.

/src/Nav/NavItem.jsx
import React from "react";
import styled from "styled-components";

const Item = styled.li`
  margin: 8px;
`;

function NavItem({ children, disabled = false }) {
  return <Item role={disabled ? "presentation" : null}>{children}</Item>;
}

export default NavItem;

유저가 클릭할 수 있는 네비게이션 링크를 나타내는 <NavLink/> 컴포넌트는 HTML의 <a> 요소를 기반으로 작성합니다.

to prop은 유저를 이동시킬 페이지의 주소를 나타내고, active prop은 주소의 활성화 여부를 결정합니다. 활성화된 링크에는 글자 색상과 굵기에 변화주고, 마우스 포인터가 올라간 링크에는 배경색과 글자색에 반전 효과를 주겠습니다.비활성환 링크는 글자색을 좀 어둡게 처리해줍니다.

웹 접근성 측면에서 현재 페이지에 해당하는 링크에는 aria-current 속성을 page로 설정해주는 것이 좋습니다. 스크린 리더(screen reader) 사용자는 브라우저 사용자처럼 어떤 링크가 현재 페이지에 해당하는지 시각적으로 알기가 어렵기 때문입니다.

/src/Nav/NavLink.jsx
import styled, { css } from "styled-components";

function isCurrent(to) {
  return window.location.pathname.startsWith(to);
}

const Link = styled.a`
  display: block;
  margin: 0 calc(20px * -1);
  padding: 8px 20px;
  border-radius: 4px;
  color: #fffffe;
  text-decoration: none;

  ${(p) =>
    p.active &&
    css`
      color: #ff8906;
      font-weight: bold;
    `}

  &:hover {
    background: #ff8906;
    color: #fffffe;
    transform: translateY(-2px);
    transition: 1s;
  }

  &:not([href]) {
    color: #a7a9be;
    background: revert;
    transform: none;
  }
`;

function NavLink({ children, to, active = false }) {
  return (
    <Link
      href={to}
      active={active}
      aria-current={isCurrent(to) ? "page" : null}
    >
      {children}
    </Link>
  );
}

export default NavLink;

네비게이션 UI 내에서 구분선을 표현하는 <NavSeparator/> 컴포넌트는 HTML의 hr 요소를 기반으로 작성합니다.

웹 접근성 측면에서 구분선은 리스트에서 항상 제외되야 하므로 role 속성을 presentation으로 지정하는 부분 외에는 큰 특이 사항은 없습니다.

/src/Nav/NavSeparator.jsx
import styled from "styled-components";

const Separator = styled.hr`
  margin: 0;
  padding: 0;
  border: 0;
  height: 1px;
  border-top: 1px solid #fffffe;
`;

function NavSeparator() {
  return <Separator role="presentation" />;
}

export default NavSeparator;

컴파운드 컴포넌트로 묶어서 내보내기

마지막으로 여태까지 작성한 모든 컴포넌트를 컴파운트 컴포넌트로 묶어서 내보기가 하겠습니다.

/src/Nav/index.js
import Nav from "./Nav";
import NavList from "./NavList";
import NavItem from "./NavItem";
import NavLink from "./NavLink";
import NavSeparator from "./NavSeparator";

Nav.List = NavList;
Nav.Item = NavItem;
Nav.Link = NavLink;
Nav.Separator = NavSeparator;

export default Nav;

위와 같이 <Nav/> 컴포넌트의 속성으로 <NavList/><NavItem/>, <NavLink/>, <NavSeparator/>를 모두 할당해서 내보내면(export) 컴포넌트를 사용할 때 <Nav/> 컴포넌트만 불러와서(import) 편하게 쓸 수 있습니다.

전체 코드

본 포스팅에서 작성한 코드는 아래에서 직접 확인하고 실행해볼 수 있습니다.

마치면서

지금까지 React와 Styled Components를 이용하여 다단계 사이드 네비게이션 UI를 구현해보았습니다. 예제 코드에서는 임의로 window.location.pathname을 사용해서 라우팅 처리를 하여 링크를 클릭할 때 마다 화면이 깜빡이는데요. 실제 React 프로젝트에서는 React Router와 같은 라이브러리를 이용하는 것이 더 일반적이오니 참고바랍니다.