๊ณ ์ •๋œ ํ—ค๋”์™€ ์ปจํ…์ธ  ๊ฐ€๋ ค์ง ํ•ด๊ฒฐ

zoomkoding๋‹˜์ด ๋ฐฐํฌํ•ด์ฃผ์‹  ๊ฐœ์ธ ๋น„ ๋ธ”๋กœ๊ทธ ํ…Œ๋งˆ๋ฅผ ์กฐ๊ธˆ์”ฉ ์ปค์Šคํ…€ํ•ด์„œ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค. ๊น”๋”ํ•œ ์›๋ณธ ํ…Œ๋งˆ ๋•๋ถ„์— ๋”ฐ๋กœ ์†์ด ๊ฐˆ ์ผ์€ ๋ณ„๋กœ ์—†์ง€๋งŒ ๊ทธ๋ž˜๋„ ์—…๊ทธ๋ ˆ์ด๋“œ ๋˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์€ ๋ถ€๋ถ„์ด ๋ณด์—ฌ ์ง์ ‘ ๋ฆฌ๋ชจ๋ธ๋ง์„ ํ•œ๋‹ค. ๋ธ”๋กœ๊ทธ ์ปค์Šคํ…€์˜ ๋งŽ์€ ๋ถ€๋ถ„์€ devhac ๋ธ”๋กœ๊ทธ๋ฅผ ์ฐธ๊ณ ํ•˜์˜€๋‹ค. hakyung๋‹˜๊ป˜ ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค!!

์ด๋ฒˆ์— ์ˆ˜์ •ํ•œ ๋ถ€๋ถ„์€ ํ—ค๋”๋ถ€๋ถ„์ด๋‹ค. ์Šคํฌ๋กค์„ ๋‚ด๋ ค๊ฐ€๋ฉด์„œ ๊ธ€์„ ์ฝ๋‹ค๋ณด๋ฉด ๋น ๋ฅด๊ฒŒ ์œ„๋กœ ์˜ฌ๋ผ๊ฐ€๊ณ  ์‹ถ๊ฑฐ๋‚˜ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฉ”๋‰ด๋ฐ”๋ฅผ ์„ ํƒํ•˜๊ณ  ์‹ถ์„ ๋•Œ๊ฐ€ ์žˆ์–ด ์›๋ž˜๋Š” Top ๋ฒ„ํŠผ์„ ๋งŒ๋“ค๋ ค๊ณ  ํ–ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ Top ๋ฒ„ํŠผ๋ณด๋‹ค๋Š” ์ƒ๋‹จ์— ๊ณ ์ •๋˜์–ด ์–ธ์ œ๋“ ์ง€ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ํ—ค๋”๊ฐ€ ๋” ์‹ค์šฉ์ ์ผ ๊ฒƒ ๊ฐ™์•„ ๊ณ ์ •๋œ ํ—ค๋”๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์œผ๋กœ ๋ฐฉํ–ฅ์„ ๋ฐ”๊พธ์—ˆ๋‹ค.

์ด ๊ธ€์—์„œ๋Š” ๋ฆฌ์•กํŠธ๋กœ ๊ณ ์ •๋œ ํ—ค๋”๋ฅผ ๋งŒ๋“œ๋Š” ๊ณผ์ •๊ณผ, css๋กœ ๊ณ ์ •๋œ ํ—ค๋”์˜ ๊ณ ์งˆ์ ์ธ ํ•œ๊ณ„์ธ ์ปจํ…์ธ  ๊ฐ€๋ ค์ง ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋‚ด์šฉ์„ ์†Œ๊ฐœํ•œ๋‹ค.

์Šคํฌ๋กค ์‹œ ๊ณ ์ •๋œ ํ—ค๋”(Sticky Header on Scroll) ๋งŒ๋“ค๊ธฐ

์›๋ฆฌ

๊ณ ์ •๋œ ํ—ค๋”๋ฅผ ๋งŒ๋“œ๋Š” ์›๋ฆฌ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. ์Šคํฌ๋กค์ด ๋˜์—ˆ๋Š”์ง€ ์—ฌ๋ถ€(true, false)๋ฅผ ๋ณ€์ˆ˜๋กœ ์ €์žฅํ•ด๋‘”๋‹ค. ์ตœ์ดˆ์—๋Š” false๋กœ ์ €์žฅํ•œ๋‹ค.
  2. 1์—์„œ ์ง€์ •ํ•œ ๋ณ€์ˆ˜์— ์Šคํฌ๋กค์ด ์•ˆ๋˜์–ด์žˆ๋Š” ์ƒํƒœ(false)๊ฐ€ ์ €์žฅ๋˜์–ด ์žˆ๊ณ , ํ˜„์žฌ ์œˆ๋„์šฐ์—์„œ ์Šคํฌ๋กค์ด ๊ฐ์ง€๋˜๋ฉด ํ—ค๋”๋ฅผ ์ƒ๋‹จ์— ๊ณ ์ •์‹œํ‚จ๋‹ค. ์ดํ›„ 1์—์„œ ์ง€์ •ํ•œ ๋ณ€์ˆ˜์—๋Š” ์Šคํฌ๋กค์ด ๋˜์–ด์žˆ์Œ(true)์„ ์ €์žฅํ•œ๋‹ค.
  3. ๋งŒ์•ฝ 1์—์„œ ์ง€์ •ํ•œ ๋ณ€์ˆ˜์— ์Šคํฌ๋กค์ด ๋˜์–ด์žˆ๋Š” ์ƒํƒœ(true)๊ฐ€ ์ €์žฅ๋˜์–ด ์žˆ๊ณ , ํ˜„์žฌ ์œˆ๋„์šฐ์—์„œ ์Šคํฌ๋กค์ด ๊ฐ์ง€๋˜์ง€ ์•Š์œผ๋ฉด ํ—ค๋”๋ฅผ ๊ณ ์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค. ์ดํ›„ 1์—์„œ ์ง€์ •ํ•œ ๋ณ€์ˆ˜์—๋Š” ์Šคํฌ๋กค์ด ์•ˆ๋˜์–ด์žˆ์Œ(false)์„ ์ €์žฅํ•œ๋‹ค.

๊ตฌํ˜„

์œ„์—์„œ ์„ค๋ช…ํ•œ ๋‚ด์šฉ์„ ๋ฆฌ์•กํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•˜์—ฌ ๋‹จ๊ณ„๋ณ„๋กœ ๊ตฌํ˜„ํ•œ๋‹ค.

1. ์Šคํฌ๋กค ์—ฌ๋ถ€ ๋ณ€์ˆ˜ ์ง€์ •
JavaScript
import React, { useState, useEffect } from 'react';

const [scrolled, setScrolled] = useState(false);

useState ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•˜์—ฌ ํ˜„์žฌ ์Šคํฌ๋กค ์—ฌ๋ถ€ ๋ณ€์ˆ˜(scrolled)๋ฅผ false๋กœ ์ดˆ๊ธฐํ™”ํ•˜๊ณ  ์ด๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ํ•จ์ˆ˜(scrolled)๋ฅผ ์„ ์–ธํ•œ๋‹ค.


2. ํ˜„์žฌ ์Šคํฌ๋กค ์ƒํƒœ์— ๋”ฐ๋ผ ์Šคํฌ๋กค ์—ฌ๋ถ€ ๋ณ€์ˆ˜ ์žฌ์ง€์ •
JavaScript
import React, { useState, useEffect } from 'react';

useEffect(() => {
    const handleScroll = () => {
      if (!scrolled && window.pageYOffset > 30) {
        setScrolled(true);
      } else if (scrolled && window.pageYOffset <= 30) {
        setScrolled(false);
      }
    };
    
    window.addEventListener('scroll', handleScroll);

    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, [scrolled]);

  // return ์ƒ๋žต
  ...
};
  1. useEffect ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•œ๋‹ค. ์ด๋•Œ ์ฒซ๋ฒˆ์งธ ์ธ์ž๋Š” ์Šคํฌ๋กค ์—ฌ๋ถ€ ๋ณ€์ˆ˜์™€ ํ—ค๋” ๊ณ ์ •์„ ์žฌ์ง€์ •ํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ, ๋‘๋ฒˆ์งธ ์ธ์ž๋Š” useEffect ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ• ์ง€ ๋ง์ง€ ๊ฒฐ์ •ํ•˜๊ธฐ ์œ„ํ•œ ์กฐ๊ฑด ๋ณ€์ˆ˜๋ฅผ ์ง€์ •ํ•œ๋‹ค.
  2. ์ฒซ๋ฒˆ์งธ ์ธ์ž - handleScrollํ•จ์ˆ˜์™€ clean-up ๋ถ€๋ถ„

    • handleScrollํ•จ์ˆ˜

      • ์Šคํฌ๋กค ๊ฐ์ง€๋ฅผ ์œ„ํ•ด์„œ๋Š” ํ™”๋ฉด ์ƒ๋‹จ์œผ๋กœ๋ถ€ํ„ฐ ์Šคํฌ๋กคํ•œ ๊ฑฐ๋ฆฌ๋ฅผ ๊ตฌํ•˜๋Š” pageYOffset ์†์„ฑ์„ ์ด์šฉํ•˜์—ฌ ์ด ๊ฑฐ๋ฆฌ ๊ฐ’์ด 30์„ ๋„˜๋Š”์ง€ ์•ˆ๋„˜๋Š”์ง€๋ฅผ ํ™•์ธํ•จ
      • if๋ฌธ์„ ํ†ตํ•ด ํ™”๋ฉด ์ƒ๋‹จ์œผ๋กœ๋ถ€ํ„ฐ ์Šคํฌ๋กค์ด ์ด๋ฃจ์–ด์ง€๊ณ  ์žˆ๋Š” ๊ฒฝ์šฐ๋ฅผ ํŒ๋ณ„
      • else if๋ฌธ์„ ํ†ตํ•ด ํ™”๋ฉด ์ค‘๊ฐ„๋ถ€๋ถ„์—์„œ ์ƒ๋‹จ์œผ๋กœ ์˜ฌ๋ผ๊ฐ€๋Š” ์Šคํฌ๋กค์„ ํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ํŒ๋ณ„
    • clean-up

      • ์Šคํฌ๋กค ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ handleScrollํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœ
      • useEffect ํ•จ์ˆ˜๋ฅผ ๊ณ„์†ํ•ด์„œ ์žฌ์‹คํ–‰ํ•ด์•ผํ•˜๋ฏ€๋กœ ์ด์ „์— ๋ฐœ์ƒํ–ˆ๋˜ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ์ •๋ฆฌํ•จ
  3. ๋‘๋ฒˆ์งธ ์ธ์ž - [scrolled] ๋ณ€์ˆ˜

    • ๊ณ„์†ํ•ด์„œ ์žฌ์‹คํ–‰๋˜๋Š” useEffect ํ•จ์ˆ˜๋Š” ์„ฑ๋Šฅ์„ ์ €ํ•˜์‹œํ‚ฌ ์ˆ˜ ์žˆ์Œ
    • ์ด๋ฅผ ๋ง‰๊ธฐ์œ„ํ•ด ์ธ์ž๋กœ [scrolled] ๋ณ€์ˆ˜๋ฅผ ๋„˜๊ฒจ์ฃผ๊ณ  ๋ Œ๋”๋ง ์ด์ „๊ณผ ์ดํ›„์˜ [scrolled] ๊ฐ’์„ ๋น„๊ตํ•˜์—ฌ ๋™์ผํ•  ๊ฒฝ์šฐ useEffect ํ•จ์ˆ˜๋ฅผ ๊ฑด๋„ˆ๋›ฐ๋„๋ก ํ•จ

3. ํ—ค๋”๋ฅผ ๊ณ ์ •์‹œํ‚ค๋Š” ํšจ๊ณผ ๋‚˜ํƒ€๋‚ด๊ธฐ

ํด๋ž˜์Šค ๋ช…์— ๋”ฐ๋ผ style์„ ๋‹ค๋ฅด๊ฒŒ ์„ค์ •ํ•ด์ฃผ๋ฉด ์Šคํฌ๋กค ์—ฌ๋ถ€์— ๋”ฐ๋ผ ํ—ค๋”๋ฅผ ๊ณ ์ •์‹œํ‚ค๋Š” ํšจ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ผ ์ˆ˜ ์žˆ๋‹ค.

HTML
<header className={scrolled ? "page-header-wrapper scrolled" : "page-header-wrapper"}>
    < !-- html ๊ตฌํ˜„ ์ƒ๋žต -- >
    ...
</header>
CSS
.page-header-wrapper {
  position: fixed;
  display: flex;
  justify-content: center;
  top: 0;
  width: 100%;
  background-color: #fff;
  z-index: 1;
  @include content-horizontal-padding;

  // ์Šคํฌ๋กค ์ด๋ฒคํŠธ ์‹œ
  &.scrolled {
    box-shadow: 1px 2px 18px rgba(0,0,0,.1);

    .page-header {
      height: 75px;
      transition: height 0.3s ease;
    }
  }

  // ์Šคํฌ๋กค ์ด๋ฒคํŠธ ์—†์„ ์‹œ
  .page-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    height: 100px;
    @include content-max-width;
  }

  // css ์ƒ๋žต
  ...
}
  • ์‚ผํ•ญ ์กฐ๊ฑด ์—ฐ์‚ฐ์ž๋ฅผ ์ด์šฉํ•˜์—ฌ scrolled ๋ณ€์ˆ˜์˜ ์ฐธ ๊ฑฐ์ง“ ์—ฌ๋ถ€์— ๋”ฐ๋ฅธ <header> ํƒœ๊ทธ์˜ ํด๋ž˜์Šค๋ช…์„ ์ง€์ •ํ•ด์ค€๋‹ค.
  • ์Šคํฌ๋กค ์ด๋ฒคํŠธ ๋ฐœ์ƒ ์‹œ ํ—ค๋”์˜ ๊ธธ์ด๋ฅผ ์ค„์ด๋Š” ํšจ๊ณผ๋ฅผ ์ฃผ๊ณ  ๊ทธ๋ฆผ์ž ํšจ๊ณผ๋ฅผ ๋„ฃ์—ˆ๋‹ค.

์ „์ฒด ์ฝ”๋“œ

src/components/page-header/index.js
import { Link } from 'gatsby';
import PropTypes from 'prop-types';
import React, { useState, useEffect } from 'react';

import headerTitleImg from '../../assets/capo-profile-img.png'

import './style.scss';

const PageHeader = ({ siteTitle }) => {
  const [scrolled, setScrolled] = useState(false);

  useEffect(() => {
    const handleScroll = () => {
      if (!scrolled && window.pageYOffset > 30) {
        setScrolled(true);
      } else if (scrolled && window.pageYOffset <= 30) {
        setScrolled(false);
      }
    };
    
    window.addEventListener('scroll', handleScroll);

    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, [scrolled]);

  return (
    <header className={scrolled ? "page-header-wrapper scrolled" : "page-header-wrapper"}>
      <div className="page-header">
        <div className="front-section">
          <Link className="link" to="/">
            <div className="front-section-img-title-wrapper">
              <img className="front-section-img" src={headerTitleImg} />
              <div className="front-section-title">
                {siteTitle}
              </div>
            </div>
          </Link>
        </div>
        <div className="trailing-section">
          <Link className="link" to="/about">
            about
          </Link>
          <Link className="link" to="/posts">
            posts
          </Link>
        </div>
      </div>
    </header>
  );
};

PageHeader.propTypes = {
  siteTitle: PropTypes.string,
};

PageHeader.defaultProps = {
  siteTitle: ``,
};

export default PageHeader;

๊ณ ์ •๋œ ํ—ค๋”์— ๊ฐ€๋ ค์ง„ ์ปจํ…์ธ  ๊ตฌ์ถœํ•˜๊ธฐ

โ€??? : ํ—ค๋”๊ฐ€ ์˜ˆ์˜๊ฒŒ ์˜ฌ๋ผ๊ฐ€๋ฉด ๋ญํ•ด,,, ๋‚ด์šฉ์ด ์•ˆ๋ณด์ด๋Š”๋ฐ;;โ€

์ด๋Ÿด๋•Œ๋Š” ํ—ค๋” ๋ฐ”๋กœ ๋‹ค์Œ์— ์œ„์น˜ํ•œ ์ปจํ…์ธ ๊ฐ€ ํ—ค๋” ๋†’์ด๋งŒํผ ๋‚ด๋ ค์˜ค๊ฒŒ ํ•˜๋ฉด ๋œ๋‹ค. ๋‹จ, ๋ธ”๋กœ๊ทธ ํŽ˜์ด์ง€ ์ค‘ ํ—ค๋” ๋‹ค์Œ์— ์œ„์น˜ํ•œ ๋ชจ๋“  ์ปจํ…์ธ  ํƒœ๊ทธ์— ํšจ๊ณผ๋ฅผ ์ ์šฉํ•ด์•ผ ํ•œ๋‹ค.

ํ•ด๋‹น ๋ธ”๋กœ๊ทธ๋Š” ํฌ์ŠคํŠธ์˜ ํ—ค๋”, about ํŽ˜์ด์ง€, index ํŽ˜์ด์ง€, table-of-contents(๋ชฉ์ฐจ)์˜ ์Šคํƒ€์ผ ๋ถ€๋ถ„์— padding-top: 100px;์„ ์ ์šฉํ•˜์—ฌ ํ•ด๊ฒฐํ–ˆ๋‹ค.

๊ณ ์ •๋œ ํ—ค๋”์— ๊ฐ€๋ ค์ง„ ์•ต์ปค(์ œ๋ชฉ ํƒœ๊ทธ) ๊ตฌ์ถœํ•˜๊ธฐ

์•„์ง ๋๋‚œ๊ฒŒ ์•„๋‹ˆ๋‹ค ^^;;; ๋ชจ๋“  ์ปจํ…์ธ ๊ฐ€ ์ž˜ ๋ณด์ด๋Š”๊ฐ€ ํ–ˆ๋”๋‹ˆ table-of-contents์—์„œ ์•ต์ปค๋กœ ์—ฐ๊ฒฐ๋œ <h1> ~ <h6> ํƒœ๊ทธ๋กœ ์ด๋™ ์‹œ ํ•ด๋‹น ํƒœ๊ทธ ๋ถ€๋ถ„์ด ์ •ํ™•ํžˆ ํ™”๋ฉด ์ƒ๋‹จ์— ๋”ฑ ๋งž๊ฒŒ ์ด๋™๋˜๋Š” ๋ฐ”๋žŒ์— ์• ๋งคํ•˜๊ฒŒ ์ œ๋ชฉ์ด ๋ณด์ด์ง€ ์•Š๋Š” ๋ฌธ์ œ๊ฐ€ ๋‚จ์•„์žˆ์—ˆ๋‹ค.

ํ•˜์ง€๋งŒ ํ•˜๋Š˜ ์•„๋ž˜ ์ƒˆ๋กœ์šด ๊ฒƒ์€ ์—†๋‹ค๊ณ  ์—ญ์‹œ ์Šคํƒ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ์— ์ด๋Ÿฐ ๋ฌธ์ œ๋ฅผ ํ˜ธ์†Œํ•˜๋Š” ์‚ฌ๋žŒ์ด ์žˆ์—ˆ๋‹ค. ํ•ด๊ฒฐ๋ฒ•์€ ์ œ๋ชฉ ํƒœ๊ทธ์— ::before ๊ฐ€์ƒ ์š”์†Œ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ๊ทธ ๊ธธ์ด๋ฅผ ๊ณ ์ •๋œ ํ—ค๋”์˜ ๊ธธ์ด๋งŒํผ์œผ๋กœ ์ง€์ •ํ•˜์—ฌ ๋ˆˆ์†์ž„์„ ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

CSS
h1::before, h2::before, h3::before, h4::before, h5::before, h6::before {
  display: block;
  content: " ";
  height: 90px;
  margin-top: -90px;
  visibility: hidden;
}

๊ณ ์ •๋œ ํ—ค๋”์˜ ๊ธธ์ด๋งŒํผ ์ œ๋ชฉ ํƒœ๊ทธ ์•ž์— ๊ฐ€์ƒ์˜ ์š”์†Œ๋ฅผ ์„ค์ •ํ•˜๊ณ  ํƒœ๊ทธ์˜ ๊ฐ€์‹œ์„ฑ์€ ์ˆจ๊น€์œผ๋กœ ์„ค์ •ํ•˜๋ฉด ํ™”๋ฉด์— ๋ณด์ผ๋•Œ๋Š” ์ด์ „๊ณผ ๋™์ผํ•œ ๊ตฌ์„ฑ์ด์ง€๋งŒ table-of-contents์— ์—ฐ๊ฒฐ๋œ ์•ต์ปค๋กœ ์ด๋™ํ•œ ์ œ๋ชฉ ํƒœ๊ทธ๋Š” ํ—ค๋”์— ๊ฐ€๋ ค์ง€์ง€ ์•Š๊ณ  ์ž˜ ๋ณด์ธ๋‹ค!

References

hakyung๋‹˜์˜ ๋ธ”๋กœ๊ทธ devhak
Dale Seo๋‹˜์˜ ๋ธ”๋กœ๊ทธ < DaleSeo />

React ๊ณต์‹๋ฌธ์„œ - Hook ๊ฐœ์š”
React ๊ณต์‹๋ฌธ์„œ - Using the Effect Hook
MDN Web Docs - Window.pageYOffset

stack overflow - offsetting an html anchor to adjust for fixed header