[Next.js 탐험기] 7. Next.js + 티스토리 API 블로그 만들기 - useState,useEffect 사용하여 모바일 반응형 사이드바 만들기

서론

포스팅 제목 짓기 힘들다. 이번에 해 볼 것은 React의 상태 변화 (useState)와 사이드 이펙트 (useEffect)를 사용하여 모바일 화면에서 메뉴 펼치기 버튼을 클릭하면 사이드바가 표시되게 하고, 사이드바 이외의 공간을 클릭할 시에는 사이드바가 비활성화되는 이른바 "모바일 반응형 사이드바"를 만들어 본다.

 

상태 병화와 사이드 이펙트 등에 대한 기초 설명이 필요하다면 아래 포스팅을 읽어보길 바란다.

 

 

Backend 개발자를 위한 React 기초 정리

서론 대부분의 백엔드 개발자들은 javascript와 jquery만을 사용해서 충분히 웹 사이트를 만들 수 있다. 때문에 시간이 생기면 새로운 infrastructure(인프라) 관련 cloud service를 비롯해 database의 효율적인

min-nine.tistory.com

 

반응형 CSS 추가

모바일 반응형 사이트를 만들려면 css의 미디어쿼리를 사용하면 된다. max-width 값을 설정하면 가로가 설정한 값 이하로 떨어졌을 때 해당 css가 적용된다.

/* 모바일 화면을 위한 미디어 쿼리 */
@media screen and (max-width: 600px) {
    .sideBar {
        width: 0px;
        display: none; /* 모바일 화면에서 사이드바 숨기기 */
        /* 필요한 경우 여기에 추가 모바일 스타일 작성 */
    }

    /* 모바일 화면에서 사이드바가 표시되는 스타일 (토글될 때) */
    .sideBarOpen {
        position: fixed;
        top: 0;
        left: 0;
        width: 300px; /* 사이드바의 너비를 설정 */
        height: 100%;
        overflow-y: auto;
        background-color: #333;
        color: white;
        padding: 10px;
    }

    /* 모바일 화면에서의 다른 스타일들 */
    .navItem, .subNavItem {
        padding: 10px;
        font-size: 0.8em; /* 폰트 크기 변경 등 */
    }
}

 

 

 

 

Navigation, Sidebar 부모 컴포넌트에 상태 관리 추가

NavBar.js 컴포넌트에서 Sidebar 상태를 관리하고, 이 상태를 SideBar.js 컴포넌트에서 사용하려면, React의 상태 리프팅(state lifting) 기법을 사용할 수 있다. 상태 리프팅은 공통 부모 컴포넌트에서 상태를 관리하고, 필요한 자식 컴포넌트로 상태와 상태를 변경하는 함수를 props로 전달하는 방식을 뜻한다.

공통 부모 컴포넌트에서 상태 관리: NavBar와 SideBar를 포함하는 공통 부모 컴포넌트에서 showSideBar 상태를 관리한다.

import React, { useState } from 'react';
import NavBar from './NavBar';
import SideBar from './SideBar';

const App = () => {
    const [showSideBar, setShowSideBar] = useState(false);

    return (
        <div>
            <NavBar showSideBar={showSideBar} setShowSideBar={setShowSideBar} />
            <SideBar showSideBar={showSideBar} />
            {/* 나머지 컴포넌트 */}
        </div>
    );
};

export default App;

 

상태와 상태 변경 함수를 자식 컴포넌트로 전달: showSideBar 상태와 setShowSideBar 함수를 NavBar와 SideBar에 props로 전달한다.

const NavBar = ({ showSideBar, setShowSideBar }) => {
    const toggleSideBar = () => {
        setShowSideBar(!showSideBar);
    };

    // 나머지 컴포넌트 구조
};

export default NavBar;



const SideBar = ({ showSideBar }) => {
    // showSideBar 상태를 사용하여 사이드바 표시 여부 결정
    // 예: className={showSideBar ? 'active' : ''}

    // 나머지 컴포넌트 구조
};

export default SideBar;

 

위 방식으로 NavBar에서 사이드바의 표시 상태를 변경하면, 해당 상태가 SideBar 컴포넌트에도 반영된다. 이는 React에서 상태를 여러 컴포넌트 간에 공유하고 관리하는 효과적인 방법이다.

 

 

 

부모 컴포넌트에서 상태관리

위에서 설명한 내용을 기반으로, NavBar와 SideBar를 관리하는 부모 컴포넌트에서 상태 관리(useState)에 대한 내용을 정의한다. 나는 showMobileSideBar 라는 상태관리 변수를 선언하고, setShowMobileSideBar라는 변수값 변경용 함수를 명칭했다. 초기값은 false로 주었다.

"use client"
import React, { useState } from 'react';

import NavBar from '../components/NavBar/NavBar';
import Footer from '../components/Footer/Footer';
import SideBar from '../components/SideBar/SideBar';
import layoutContainer from './main.module.css';

const Main = ({children}) => {
    const [showMobileSideBar, setShowMobileSideBar] = useState(false);

    return (
        <div className={layoutContainer.container}>
            <aside className={layoutContainer.sideBar}>
                <SideBar showMobileSideBar={showMobileSideBar} setShowMobileSideBar={setShowMobileSideBar}/>
            </aside>
            <header className={layoutContainer.header}>
                <NavBar showMobileSideBar={showMobileSideBar} setShowMobileSideBar={setShowMobileSideBar}/>
            </header>
            <main className={layoutContainer.mainContent}>
                {children}
            </main>
            <footer className={layoutContainer.footer}>
                <Footer/>
            </footer>
        </div>
    );
}

export default Main

그 후에 각각 자식 컴포넌트인 SideBar와 NavBar에 상태관리 변수 및 함수를 props로 넘겨주었다.

 

 

NavBar 코드 변경

넘겨받은 props를 중괄호 {}를 사용하여 받아오고, 메뉴 펼치기 버튼을 클릭할 때마다 setShowMobileSideBar(true)를 사용하여 showMobileSideBar 상태 변수의 값을 true로 변경해줬다.

"use client"

import React, {useState, useEffect} from 'react';
import Link from 'next/link';
import styles from './NavBar.module.css'; // CSS 모듈을 사용하려면 먼저 생성해야 한다.

const NavBar = ({showMobileSideBar, setShowMobileSideBar}) => {

    const toggleSideBar = () => {
        // SideBar의 토글 로직을 여기에 구현
        setShowMobileSideBar(true);
    };

    return (
        <nav className={styles.navBar}>
            <button className={styles.menuToggle} onClick={toggleSideBar}>
                메뉴 펼치기
            </button>
            <ul className={styles.navList}>
                <li className={styles.navItem}>
                    <Link className={styles.navLink} href="/">
                        Home
                    </Link>
                </li>
                // 이하 코드 생략 ...
            </ul>
        </nav>
    );
};

export default NavBar;

 

 

SideBar 코드 변경

SideBar에서는 해당 상태 변화 변수 값이 true일 경우 sideBarOpen이라는 클래스명을 적용시켜주었다. 

return (
        <nav className={` ${showMobileSideBar ? styles.sideBarOpen :styles.sideBar}`}>
            <ul className={styles.navList}>
                {categoriesData.categories.map((category) => (
                    // 이하 코드 생략 ..
                ))}
            </ul>
        </nav>
    );

 

 

 

Sidebar 이외 영역 클릭시 SideBar 닫기

문제는 SideBar가 열린 후, 닫히지 않는다는 것이다. 때문에 SideBar 영역 이외의 공간을 클릭하면 자동으로 닫을 필요가 있다. 사이드바 이외의 영역을 클릭했을 때 사이드바를 닫으려면, 페이지 전체에 이벤트 리스너를 추가하고 사이드바 외부 영역의 클릭을 감지하는 방법을 사용할 수 있다. 여기서 중요한 것은 사이드바 내부에서 발생하는 이벤트가 외부 클릭으로 간주되지 않도록 하는 것이다.

 

때문에 이벤트 리스트너를 설정해야 한다. document에 클릭 이벤트 리스너를 추가하고, 클릭이 사이드바 외부에서 발생했는지 확인 한다.
또한 사이드바 내부에서 클릭 이벤트가 발생했을 때는 이벤트 버블링을 중지하여 외부 클릭으로 간주되지 않도록 한다.

"use client"
// components/SideBar/SideBar.js

import Link from 'next/link';
import React, { useState, useEffect, useRef } from 'react';
import styles from './SideBar.module.css';


const SideBar = ({showMobileSideBar, setShowMobileSideBar}) => {
    const sideBarRef = useRef();
    const [categoriesData, setCategoriesData] = useState(null);

    useEffect(() => {
        fetch('/data/categories.json', { next: { revalidate: 3600 } })
            .then(response => response.json())
            .then(data => setCategoriesData(data))
            .catch(error => console.error('Error loading categories:', error));
    }, []);

    useEffect(() => {
        const handleClickOutside = (event) => {
            if (sideBarRef.current && !sideBarRef.current.contains(event.target)) {
                setShowMobileSideBar(false);
            }
        };

        // 전체 페이지에 클릭 이벤트 리스너 추가
        document.addEventListener('click', handleClickOutside);

        // 클린업 함수
        return () => {
            document.removeEventListener('click', handleClickOutside);
        };
    }, [showMobileSideBar])

    if (!categoriesData) {
        return <div>Loading...</div>;
    }

    return (
        <nav ref={sideBarRef} onClick={(e) => e.stopPropagation()} className={` ${showMobileSideBar ? styles.sideBarOpen :styles.sideBar}`}>
            <ul className={styles.navList}>
                {categoriesData.categories.map((category) => (
                    // 이하 코드 생량 ..
                ))}
            </ul>
        </nav>
    );
};

export default SideBar;

이 코드에서 useEffect를 사용하여 컴포넌트가 마운트될 때 이벤트 리스너를 설정하고, 컴포넌트가 언마운트될 때 이벤트 리스너를 제거한다. sideBarRef는 사이드바 DOM 요소를 참조하는 데 사용되며, handleClickOutside 함수는 클릭 이벤트가 사이드바 외부에서 발생했는지 확인한다. 클릭 이벤트가 사이드바 외부에서 발생하면 setShowSideBar(false)를 호출하여 사이드바를 닫는다.

 

 

결과물