기술/웹 개발

[next.js/typescript] bootstrap에서 scrollSpy 적용시키기

하기싫지만어떡해해야지 2024. 8. 6. 18:05

내가 선택한 개인 웹사이트 디자인에는
scrollSpy 기능이 적용되어있는 디자인이었다
 
https://getbootstrap.kr/docs/5.0/components/scrollspy/

스크롤스파이

스크롤 위치에 따라 Bootstrap 내비게이션 또는 목록 그룹 컴포넌트를 자동으로 갱신하여 뷰포트 내에서 현재 어떤 링크가 활성화 된지를 나타냅니다.

getbootstrap.kr

 
간단하게 설명하자면
웹사이트를 들어갔을 때
내가 스크롤을 내릴때마다
옆에 있는 sideBar의 메뉴들이
내 스크롤 위치에 따라 변경되는
그런 웹 사이트들을 많이 봤을 것이다
 
그런 기능을 하는 것이
scrollSpy다
 
물론 직접 코드로 구현할 수도 있지만
bootstrap에서 아주 간단하게
제공하고 있기 때문에
그 친구를 써주기로 했다


next.js와 Bootstrap과 Client Side Rendering(CSR)

우선 next.js, typescript, react 세 가지가
모두 처음이었던 내가 가장 힘들었던 부분이
클라이언트 사이드에서 bootstrap을
렌더링해야한다는 것이었다
 
next.js는 Server Side Rendering(SSR)을
지원하는 framework이기 때문에
그래서 기본적으로 클라이언트 사이드에서 쓸 수 있는
document나 window와 같은 메소드를
쓰기 어려운 것으로 알고있는데,
어떻게하면 bootstrap의 scrollSpy를
client side에서 실행되도록 할 수 있는지
조금 애를 먹었다
 
하지만 결론은 간단하다
bootstrap을 동적으로 import해서
client side에서만 사용하도록
해주면 된다


Bootstrap scrollSpy 버전

그렇게 gpt와 구글링의 도움을 받아
어떻게든 코드를 짰는데
아무리해도 scrollSpy가 적용이 안되는 것이다
 
진짜 여기서 삽질을 너무 많이했는데

결론은

bootstrap 버전 때문이었다
.....
...
https://github.com/twbs/bootstrap/issues/36431

ScrollSpy not behaving correctly · Issue #36431 · twbs/bootstrap

Scroll spy on body element will only activate the first item on the nav menu and will not update accordingly, Boostrap 5.1.3 works fine but for some reason 5.2 beta doesn't

github.com

 
위 이슈를 보면
bootstrap의 scrollSpy가
5.1.3 버전에서는 제대로 작동하는데
5.2 ~ 5.3 버전부터 제대로 작동하지 않는다는
글을 볼 수 있다
 
설마 진짜 버전 때문이겠어 생각하며
반신반의 한채로 bootstrap 버전만 바꿔서 실행해봤는데
바로 해결이 ㅎㅎ,,,
 
아무튼 bootstrap에서 scrollSpy를
적용시켜주고 싶으면
bootstrap v5.1.3으로하면
좋을 것 같다..
 
삽질 결론부터 먼저 안 채로
우선 하나하나 차근차근 코딩 시작


1. Bootstrap 설치

npm install bootstrap@v5.1.3

 
우선 next.js 프로젝트에 bootstrap을
v5.1.3으로 설치해주자
 

2. scrollSpy 적용시킬 component 만들기

나는 start bootstrap에서 다운받은
html, css, js를 이용했다
 
우선 내 프로젝트는 typescript 였으므로
js로 적혀있는 코드를 typescript로 바꾸어주었다
(thanks to GPT 1초컷)
 
그런 다음 컴포넌트로 tsx파일을
새로 생성해서 작성해주었다
 
 
이건 다운받았던 기존 js코드

/*!
* Start Bootstrap - Resume v7.0.6 (https://startbootstrap.com/theme/resume)
* Copyright 2013-2023 Start Bootstrap
* Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-resume/blob/master/LICENSE)
*/
//
// Scripts
// 

window.addEventListener('DOMContentLoaded', event => {

    // Activate Bootstrap scrollspy on the main nav element
    const sideNav = document.body.querySelector('#sideNav');
    if (sideNav) {
        new bootstrap.ScrollSpy(document.body, {
            target: '#sideNav',
            rootMargin: '0px 0px -40%',
        });
    };

    // Collapse responsive navbar when toggler is visible
    const navbarToggler = document.body.querySelector('.navbar-toggler');
    const responsiveNavItems = [].slice.call(
        document.querySelectorAll('#navbarResponsive .nav-link')
    );
    responsiveNavItems.map(function (responsiveNavItem) {
        responsiveNavItem.addEventListener('click', () => {
            if (window.getComputedStyle(navbarToggler).display !== 'none') {
                navbarToggler.click();
            }
        });
    });

});

 
위 파일은 그냥 static에 업로드해서
client side에서 적용되게하는
js파일이었으므로
이를 다르게 수정해줘야한다
 
scrollSpy가 적용되어야하는
component를 만들 때 주의해야할 점은
 
1.최상단에 "use client"; 써주기(import보다 위에)
2. React의 useEffect() 내부에서 작업 수행
3. import('bootstrap')으로 useEffect() 내부에서
동적으로 bootstrap import해주기
이다
 
1번 "use client"는
해당 모듈이 client side에서 실행되는
client component임을 명시해주는 것이다
 
이를 명시해주지않으면
[Next.js] Next.js Event handlers cannot be passed to client component
에러가 뜰 수 있다
 
2번 React의 useEffect는
함수형 컴포넌트에서 사이드 이펙트를 수행하는 훅이다
useEffect 내부에서
컴포넌트 렌더링 이외에 수행되어야하는
작업들을 작성해주어야한다
이도 client side에서 실행되도록 하는 것과
관련이 있다
 
3번처럼 bootstrap도 이러한 이유로
useEffect 내부에서 import해주어야 한다
안그러면
ReferenceError: document is not defined
에러가 발생한다
 
 
이러한 방법으로 내가 작성한
SideNav.tsx 파일

"use client";

import React, {useEffect } from 'react';
import {Statics} from "../statics";
import 'bootstrap/dist/css/bootstrap.min.css';

export default function SideNav() {
  useEffect(() => {
    // 동적으로 부트스트랩을 로드하여 클라이언트 사이드에서만 사용
    import('bootstrap').then((bootstrap) => {
      const sideNav = document.querySelector<HTMLElement>('#sideNav');
      if (sideNav) {
        new bootstrap.ScrollSpy(document.body, {
          target: '#sideNav',
          rootMargin: '0px 0px -40%',
        });
      }

      const navbarToggler = document.querySelector<HTMLElement>('.navbar-toggler');
      const responsiveNavItems = Array.from(
        document.querySelectorAll<HTMLElement>('#navbarResponsive .nav-link')
      );

      const handleNavItemClick = () => {
        if (navbarToggler && window.getComputedStyle(navbarToggler).display !== 'none') {
          navbarToggler.click();
        }
      };

      responsiveNavItems.forEach((responsiveNavItem) => {
        responsiveNavItem.addEventListener('click', handleNavItemClick);
      });

      // Clean up event listeners on component unmount
      return () => {
        responsiveNavItems.forEach((responsiveNavItem) => {
          responsiveNavItem.removeEventListener('click', handleNavItemClick);
        });
      };
    });
  }, []);
    return (
        <nav className="navbar navbar-expand-lg navbar-dark bg-primary fixed-top" id="sideNav">
          <a className="navbar-brand js-scroll-trigger" href="#page-top">
              <span className="d-block d-lg-none">{Statics.NAME.FIRST_NAME}</span>
              <span className="d-none d-lg-block"><img className="img-fluid img-profile rounded-circle mx-auto mb-2" src="assets/img/profile.jpg" alt="..." /></span>
          </a>
          <button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation"><span className="navbar-toggler-icon"></span></button>
          <div className="collapse navbar-collapse" id="navbarResponsive">
              <ul className="navbar-nav">
                  <li className="nav-item"><a className="nav-link js-scroll-trigger" href="#about">About</a></li>
                  <li className="nav-item"><a className="nav-link js-scroll-trigger" href="#experience">Experience</a></li>
                  <li className="nav-item"><a className="nav-link js-scroll-trigger" href="#education">Education</a></li>
                  <li className="nav-item"><a className="nav-link js-scroll-trigger" href="#skills">Skills</a></li>
                  <li className="nav-item"><a className="nav-link js-scroll-trigger" href="#interests">Interests</a></li>
                  <li className="nav-item"><a className="nav-link js-scroll-trigger" href="#awards">Awards</a></li>
              </ul>
          </div>
      </nav>
    );
};

 
 

3. Page.tsx에서 컴포넌트 불러오기

이렇게 useEffect를 사용해서
component 함수를 생성해주었으면
전체 html을 불러오는 부분인
Page.tsx에 불러와주면 된다
 
이때 주의해야할 점은
가장 위에

import 'bootstrap/dist/css/bootstrap.min.css';

이 부분을 넣어줘야한다

import 'bootstrap/dist/css/bootstrap.min.css';
import {Statics} from "./statics";
import SideNav from "./components/sideNav";

export default function Home() {
  return (
    <body id="page-top">
    {/* navigation */}
    <SideNav />

 
컴포넌트가 들어가야할 부분에
<컴포넌트 이름 />
이런 형식으로 넣어주면 된다
 
 
그리고 이제 실행해주면..
 

 

잘 작동되는 것을 확인해줄 수 있다..
 
 
이렇게 삽질 마무리 ㅎ
 
참고: https://www.geeksforgeeks.org/how-to-use-bootstrap-with-nextjs/

How to use Bootstrap with NextJS? - GeeksforGeeks

A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

www.geeksforgeeks.org