ChatGPT가 알려준 Intersection Observer를 활용한 ToC 코드

찐새·2023년 3월 24일
0

Javascript

목록 보기
8/11
post-thumbnail

Vanilla JS

// 대상으로 할 h2 요소들 선택
const headings = document.querySelectorAll('h2');

// 대상 요소가 viewport에 들어올 때 실행될 함수
const callback = entries => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // Intersection Observer로 관찰 중인 요소에 해당하는 링크를 가져옴
      const link = document.querySelector(`nav a[href="#${entry.target.id}"]`);
      // 링크가 존재할 경우, 활성화 클래스를 추가함
      if (link) {
        link.classList.add('active');
      }
    } else {
      // Intersection Observer로 관찰 중인 요소에 해당하는 링크를 가져옴
      const link = document.querySelector(`nav a[href="#${entry.target.id}"]`);
      // 링크가 존재할 경우, 활성화 클래스를 제거함
      if (link) {
        link.classList.remove('active');
      }
    }
  });
};

// Intersection Observer 생성
const observer = new IntersectionObserver(callback, {
  rootMargin: '-50% 0px -50% 0px' // viewport 중앙에서 50% 벗어난 지점에 위치한 요소를 관찰
});

// 각 h2 요소를 대상으로 Intersection Observer 등록
headings.forEach(heading => {
  observer.observe(heading);
});

// nav 요소 내에 목차 생성
const nav = document.querySelector('nav ul');
headings.forEach(heading => {
  const li = document.createElement('li');
  const link = document.createElement('a');
  link.href = `#${heading.id}`;
  link.textContent = heading.textContent;
  li.appendChild(link);
  nav.appendChild(li);
});

React

import { useEffect, useState, useRef } from 'react';

function TableOfContents() {
  const [headings, setHeadings] = useState([]);
  const [activeIndex, setActiveIndex] = useState(0);
  const headingsRef = useRef([]);

  useEffect(() => {
    // 페이지 내의 h2 요소들 선택
    const h2s = document.querySelectorAll('h2');
    // headings 상태 업데이트
    setHeadings([...h2s]);
  }, []);

  useEffect(() => {
    // Intersection Observer 생성
    const observer = new IntersectionObserver((entries) => {
      // entries 배열의 첫 번째 요소에 대해서만 처리함
      const index = headings.indexOf(entries[0].target);
      setActiveIndex(index);
    }, {
      // viewport 중앙에서 50% 벗어난 지점에 위치한 요소를 관찰
      rootMargin: '-50% 0px -50% 0px',
    });

    // Intersection Observer 등록
    headings.forEach((heading) => {
      observer.observe(heading);
    });

    // headingsRef.current 배열 업데이트
    headingsRef.current = headings;
  }, [headings]);

  return (
    <nav>
      <ul>
        {headings.map((heading, index) => (
          <li key={index}>
            <a
              href={`#${heading.id}`}
              className={activeIndex === index ? 'active' : ''}
            >
              {heading.textContent}
            </a>
          </li>
        ))}
      </ul>
    </nav>
  );
}

Next.js(CSR)

import { useEffect, useState, useRef } from 'react';

function TableOfContents() {
  const [headings, setHeadings] = useState([]);
  const [activeIndex, setActiveIndex] = useState(0);
  const headingsRef = useRef([]);

  useEffect(() => {
    // 페이지 내의 h2 요소들 선택
    const h2s = document.querySelectorAll('h2');
    // headings 상태 업데이트
    setHeadings([...h2s]);
  }, []);

  useEffect(() => {
    // Intersection Observer 생성
    const observer = new IntersectionObserver((entries) => {
      // entries 배열의 첫 번째 요소에 대해서만 처리함
      const index = headings.indexOf(entries[0].target);
      setActiveIndex(index);
    }, {
      // viewport 중앙에서 50% 벗어난 지점에 위치한 요소를 관찰
      rootMargin: '-50% 0px -50% 0px',
    });

    // Intersection Observer 등록
    headings.forEach((heading) => {
      observer.observe(heading);
    });

    // headingsRef.current 배열 업데이트
    headingsRef.current = headings;
  }, [headings]);

  return (
    <nav>
      <ul>
        {headings.map((heading, index) => (
          <li key={index}>
            <a
              href={`#${heading.id}`}
              className={activeIndex === index ? 'active' : ''}
            >
              {heading.textContent}
            </a>
          </li>
        ))}
      </ul>
    </nav>
  );
}

export default function MyPage() {
  return (
    <div>
      <TableOfContents />
      <h1>Welcome to my page</h1>
      <section>
        <h2 id="section1">Section 1</h2>
        <p>Section 1 content...</p>
      </section>
      <section>
        <h2 id="section2">Section 2</h2>
        <p>Section 2 content...</p>
      </section>
      <section>
        <h2 id="section3">Section 3</h2>
        <p>Section 3 content...</p>
      </section>
    </div>
  );
}

Next.js(SSR)

import { useState, useLayoutEffect } from 'react';

function TableOfContents() {
  const [headings, setHeadings] = useState([]);
  const [activeIndex, setActiveIndex] = useState(0);

  function handleIntersectionChange(event) {
    if (event.isIntersecting) {
      const index = headings.indexOf(event.target);
      setActiveIndex(index);
    }
  }

  useLayoutEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach(handleIntersectionChange);
      },
      { rootMargin: '-50% 0px -50% 0px', threshold: 1 }
    );

    headings.forEach((heading) => observer.observe(heading));

    return () => {
      headings.forEach((heading) => observer.unobserve(heading));
    };
  }, [headings]);

  return (
    <nav>
      <ul>
        {headings.map((heading, index) => (
          <li key={index}>
            <a
              href={`#${heading.id}`}
              className={activeIndex === index ? 'active' : ''}
            >
              {heading.textContent}
            </a>
          </li>
        ))}
      </ul>
      {headings.map((heading, index) => (
        <div key={index} ref={(ref) => setHeadings((prevHeadings) => [...prevHeadings, ref])}>
          {React.cloneElement(heading, { ref: undefined })}
        </div>
      ))}
    </nav>
  );
}

export default function MyPage() {
  return (
    <div>
      <TableOfContents />
      <h1>Welcome to my page</h1>
      <section>
        <h2 id="section1">Section 1</h2>
        <p>Section 1 content...</p>
      </section>
      <section>
        <h2 id="section2">Section 2</h2>
        <p>Section 2 content...</p>
      </section>
      <section>
        <h2 id="section3">Section 3</h2>
        <p>Section 3 content...</p>
      </section>
    </div>
  );
}

Next.js(SSR with react-intersection-observer)

import { useState } from 'react';
import dynamic from 'next/dynamic';
const IntersectionObserver = dynamic(
  () => import('react-intersection-observer'),
  { ssr: false }
);

function TableOfContents() {
  const [headings, setHeadings] = useState([]);
  const [activeIndex, setActiveIndex] = useState(0);

  function handleIntersectionChange(event) {
    if (event.isIntersecting) {
      const index = headings.indexOf(event.target);
      setActiveIndex(index);
    }
  }

  function handleHeadingRef(ref) {
    if (ref) {
      setHeadings((prevHeadings) => [...prevHeadings, ref]);
    }
  }

  return (
    <nav>
      <ul>
        {headings.map((heading, index) => (
          <li key={index}>
            <a
              href={`#${heading.id}`}
              className={activeIndex === index ? 'active' : ''}
            >
              {heading.textContent}
            </a>
          </li>
        ))}
      </ul>
      {headings.map((heading, index) => (
        <IntersectionObserver
          key={index}
          onChange={handleIntersectionChange}
          rootMargin="-50% 0px -50% 0px"
          threshold={1}
        >
          <div ref={handleHeadingRef}>
            {React.cloneElement(heading, { ref: undefined })}
          </div>
        </IntersectionObserver>
      ))}
    </nav>
  );
}

export default function MyPage() {
  return (
    <div>
      <TableOfContents />
      <h1>Welcome to my page</h1>
      <section>
        <h2 id="section1">Section 1</h2>
        <p>Section 1 content...</p>
      </section>
      <section>
        <h2 id="section2">Section 2</h2>
        <p>Section 2 content...</p>
      </section>
      <section>
        <h2 id="section3">Section 3</h2>
        <p>Section 3 content...</p>
      </section>
    </div>
  );
}
profile
프론트엔드 개발자가 되고 싶다

0개의 댓글