๐Ÿ“จ ํฌํŠธํด๋ฆฌ์˜ค ๋งˆ๋ฌด๋ฆฌ ๋‹จ๊ณ„ โ€“ ์—ฐ๋ฝ์ฒ˜ ์„น์…˜ & ์Šคํฌ๋กค ๋ผ์šฐํŒ… ๊ตฌํ˜„

์กฐ์ค€ํ˜•ยท2025๋…„ 5์›” 2์ผ
0

ํฌํŠธํด๋ฆฌ์˜ค

๋ชฉ๋ก ๋ณด๊ธฐ
8/9

ํฌํŠธํด๋ฆฌ์˜ค ์‚ฌ์ดํŠธ๋ฅผ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉด์„œ ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰์— ์ถ”๊ฐ€ํ•œ ๋‘ ๊ฐ€์ง€ ๊ธฐ๋Šฅ์ด ์žˆ๋‹ค.
๋ฐ”๋กœ ์—ฐ๋ฝ์ฒ˜(Contact) ์„น์…˜๊ณผ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฉ”๋‰ด์˜ ์Šคํฌ๋กค ๋ผ์šฐํŒ… ๋ฐ ์„น์…˜ ๊ฐ์ง€ ์ฒ˜๋ฆฌ๋‹ค.
์ด ํฌ์ŠคํŠธ์—์„œ๋Š” ์–ด๋–ป๊ฒŒ ์ด ๋‘ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ–ˆ๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  ์™œ ์ด๋ ‡๊ฒŒ ๊ตฌ์„ฑํ–ˆ๋Š”์ง€๋ฅผ ์ •๋ฆฌํ–ˆ๋‹ค.

๐Ÿ“ฎ ์—ฐ๋ฝ์ฒ˜(Contact) ์„น์…˜ ๊ตฌ์„ฑ

๋Œ€๋ถ€๋ถ„์˜ ํฌํŠธํด๋ฆฌ์˜ค ์‚ฌ์ดํŠธ๊ฐ€ ๊ทธ๋ ‡๋“ฏ, ๋งˆ์ง€๋ง‰์—๋Š” ์ด๋ฉ”์ผ๊ณผ ์†Œ์…œ ๋ฏธ๋””์–ด ๋งํฌ๋ฅผ ์ œ๊ณตํ•˜๋Š” ์„น์…˜์ด ๋“ค์–ด๊ฐ„๋‹ค.
๋‚˜๋Š” ์—ฌ๊ธฐ์— ์ด๋ฉ”์ผ ๋งํฌ, ์†Œ์…œ ๋งํฌ, ์ด๋ ฅ์„œ ๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ์„ ํฌํ•จํ–ˆ๋‹ค.

๐Ÿ“Œ ํฌํ•จํ•œ ํ•ญ๋ชฉ:

  • ์ด๋ฉ”์ผ ์ฃผ์†Œ

  • ์œ„์น˜ (๊ฐ„๋‹จํ•œ ํ…์ŠคํŠธ)

  • ์†Œ์…œ ์•„์ด์ฝ˜ (GitHub, Velog)

  • ์ด๋ ฅ์„œ ๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ

๐Ÿงฑ ๊ตฌํ˜„ ๋ฐฉ์‹:

<a href="mailto:jojh0323@gmail.com" className="text-indigo-600 hover:underline">
  jojh0323@gmail.com
</a>

์†Œ์…œ ์•„์ด์ฝ˜์€ react-icons์—์„œ ๋ถˆ๋Ÿฌ์™€ ์‚ฌ์šฉํ–ˆ๊ณ , ์•„๋ž˜์ฒ˜๋Ÿผ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ–ˆ๋‹ค:

<FaGithub style={{ color: "#333", fontSize: "24px" }} />

โœจ ์Šคํฌ๋กค ๋ผ์šฐํŒ… ๊ตฌํ˜„

NavMenu์˜ ๊ฐ ๋ฉ”๋‰ด๋ฅผ ํด๋ฆญํ–ˆ์„ ๋•Œ ํ•ด๋‹น ์„น์…˜์œผ๋กœ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์ด๋™์‹œํ‚ค๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ–ˆ๋‹ค.
์˜ˆ๋ฅผ ๋“ค์–ด, #tech, #projects ๊ฐ™์€ ์•ต์ปค๋กœ ์—ฐ๊ฒฐํ•˜๊ณ , scroll-behavior: smooth CSS ์†์„ฑ์„ ์‚ฌ์šฉํ–ˆ๋‹ค.

โœ๏ธ ํ•ต์‹ฌ ์ฝ”๋“œ:

const menus = [
  { name: "ํ™ˆ", id: "about" },
  { name: "๊ธฐ์ˆ ์Šคํƒ", id: "tech" },
  ...
];

<a href={`#${menu.id}`} className="hover:text-indigo-600 transition">
  {menu.name}
</a>

๐Ÿ” ์Šคํฌ๋กค ๊ฐ์ง€ ๊ธฐ๋ฐ˜ ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ฐ•์กฐ

์ถ”๊ฐ€๋กœ, ์Šคํฌ๋กคํ•  ๋•Œ ํ˜„์žฌ ํ™”๋ฉด์— ๋ณด์ด๋Š” ์„น์…˜์— ๋งž์ถฐ ๋„ค๋น„๊ฒŒ์ด์…˜์—์„œ ํ•ด๋‹น ๋ฉ”๋‰ด๊ฐ€ ์ž๋™์œผ๋กœ ๊ฐ•์กฐ๋˜๋„๋ก ํ–ˆ๋‹ค.
์ด๊ฑด getBoundingClientRect()๋กœ ๊ฐ ์„น์…˜์˜ ์œ„์น˜๋ฅผ ๊ณ„์‚ฐํ•ด์„œ ๊ตฌํ˜„ํ–ˆ๋‹ค.

๐Ÿ”ง ๊ตฌํ˜„ ์ฝ”๋“œ:

useEffect(() => {
  const handleScroll = () => {
    const offset = window.innerHeight / 2;
    for (const menu of menus) {
      const section = document.getElementById(menu.id);
      if (section) {
        const rect = section.getBoundingClientRect();
        if (rect.top <= offset && rect.bottom >= offset) {
          setActiveId(menu.id);
          break;
        }
      }
    }
  };

  window.addEventListener("scroll", handleScroll);
  handleScroll();
  return () => window.removeEventListener("scroll", handleScroll);
}, []);

๐Ÿ’ก ๋””์ž์ธ์ ์œผ๋กœ ๊ณ ๋ คํ•œ ์ :

  • scroll-mt-16์„ ๊ฐ ์„น์…˜์— ๋ถ€์—ฌํ•ด์„œ ํด๋ฆญ ์‹œ ์ƒ๋‹จ์— ๋”ฑ ๋ถ™์ง€ ์•Š๋„๋ก ์—ฌ๋ฐฑ ํ™•๋ณด

  • ํ™”๋ฉด ์ค‘๊ฐ„ ๊ธฐ์ค€์œผ๋กœ ๊ฐ์ง€ํ•ด์„œ UX๊ฐ€ ๋ถ€๋“œ๋Ÿฝ๊ณ  ์ง๊ด€์ 

๐Ÿง  useActiveSection ์ปค์Šคํ…€ ํ›…

  • ์‚ฌ์šฉ์ž๊ฐ€ ์Šคํฌ๋กคํ•  ๋•Œ ํ˜„์žฌ ๋ณด๊ณ  ์žˆ๋Š” ์„น์…˜์— ๋”ฐ๋ผ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฉ”๋‰ด๋ฅผ ์ž๋™์œผ๋กœ ๊ฐ•์กฐ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•ด useActiveSection์ด๋ผ๋Š” ์ปค์Šคํ…€ ํ›…์„ ๋งŒ๋“ค์—ˆ๋‹ค.

  • ์ด ํ›…์€ IntersectionObserver๋ฅผ ํ™œ์šฉํ•ด ๊ฐ ์„น์…˜์˜ ํ™”๋ฉด ์ง„์ž… ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•˜๊ณ , ๊ฐ€์žฅ ๋จผ์ € ํ™”๋ฉด ์ค‘์•™ ๊ทผ์ฒ˜์— ๋“ค์–ด์˜จ ์„น์…˜์˜ id๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

๐Ÿงฑ ๊ตฌํ˜„ ์ฝ”๋“œ (์š”์•ฝ):

export function useActiveSection(sectionIds: string[]) {
  const [activeId, setActiveId] = useState(sectionIds[0]);

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        for (const entry of entries) {
          if (entry.isIntersecting) {
            setActiveId(entry.target.id);
            break;
          }
        }
      },
      { rootMargin: "-50% 0px -50% 0px", threshold: 0.1 }
    );

    sectionIds.forEach((id) => {
      const el = document.getElementById(id);
      if (el) observer.observe(el);
    });

    return () => observer.disconnect();
  }, [sectionIds]);

  return activeId;
}

๐ŸŽฏ ์ ์šฉ ๋ฐฉ์‹:

const activeId = useActiveSection(menus.map((m) => m.id));

<a
  href={`#${menu.id}`}
  className={activeId === menu.id ? "text-indigo-600" : "text-gray-600"}
>
  {menu.name}
</a>

์ด ๋ฐฉ์‹์€ ์Šคํฌ๋กค ์œ„์น˜์— ๋”ฐ๋ผ ๋™์ ์œผ๋กœ ํ˜„์žฌ ์„น์…˜์„ ๊ฐ์ง€ํ•˜๊ณ , ํ•ด๋‹น ๋ฉ”๋‰ด๋ฅผ ๊ฐ•์กฐํ•  ์ˆ˜ ์žˆ์–ด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ํ›จ์”ฌ ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ์ง๊ด€์ ์ด๋‹ค.

๐Ÿงฉ ๊ณ ๋ฏผ๊ณผ ์„ ํƒ

  • ์—ฐ๋ฝ ํผ์„ ๋„ฃ์ง€ ์•Š์€ ์ด์œ :
    ๋Œ€๋ถ€๋ถ„์˜ ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ฉ”์ผ์ด๋‚˜ GitHub๋ฅผ ํ†ตํ•ด ์ง์ ‘ ์—ฐ๋ฝํ•˜๊ธฐ ๋•Œ๋ฌธ. ์˜คํžˆ๋ ค ํผ์€ ๋ฒˆ๊ฑฐ๋กœ์šธ ์ˆ˜ ์žˆ๋‹ค.

  • ์Šคํฌ๋กค ๋ผ์šฐํŒ… ๋„์ž… ์ด์œ :
    ๋งํฌ ํด๋ฆญ ์‹œ ์ˆœ๊ฐ„ ์ด๋™๋ณด๋‹ค ํ›จ์”ฌ ์ž์—ฐ์Šค๋Ÿฝ๊ณ , ์‚ฌ์šฉ์ž ์‹œ์„ ์˜ ํ๋ฆ„์„ ๋Š์ง€ ์•Š์•„์„œ ์ข‹์•˜๋‹ค.

  • ์Šคํฌ๋กค ๊ฐ์ง€ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ์ด์œ :
    ๋‚ด๊ฐ€ ์–ด๋–ค ์„น์…˜์„ ๋ณด๊ณ  ์žˆ๋Š”์ง€ ๋ช…ํ™•ํ•˜๊ฒŒ ์ธ์ง€ํ•  ์ˆ˜ ์žˆ์–ด, ์‚ฌ์šฉ์„ฑ๊ณผ ์ „๋ฌธ์„ฑ์ด ํ•จ๊ป˜ ์˜ฌ๋ผ๊ฐ„๋‹ค.

โœ… ๋งˆ๋ฌด๋ฆฌ

์ด๋ฒˆ ์ž‘์—…์€ ์‚ฌ์ดํŠธ์˜ ์™„์„ฑ๋„๋ฅผ ๋†’์ด๋Š” ์ค‘์š”ํ•œ ๋งˆ๋ฌด๋ฆฌ ๋‹จ๊ณ„์˜€๋‹ค.
ํŠนํžˆ ์‚ฌ์šฉ์ž๊ฐ€ ๋งˆ์ง€๋ง‰๊นŒ์ง€ ์‚ฌ์ดํŠธ๋ฅผ ๋ดค์„ ๋•Œ, ์—ฐ๋ฝํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด ๋ช…ํ™•ํ•˜๊ฒŒ ์•ˆ๋‚ด๋˜์–ด์•ผ ์ง„์งœ ํฌํŠธํด๋ฆฌ์˜ค๋ผ๊ณ  ์ƒ๊ฐํ•œ๋‹ค.
๋˜ํ•œ, ์Šคํฌ๋กค ๋ผ์šฐํŒ…๊ณผ ๊ฐ์ง€ ๊ธฐ๋Šฅ์€ ์ž์ž˜ํ•˜์ง€๋งŒ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ™• ๋Œ์–ด์˜ฌ๋ฆฌ๋Š” ํ•ต์‹ฌ ํฌ์ธํŠธ์˜€๋‹ค.

https://portfolio-kappa-rouge-28.vercel.app/

profile
์ฝ”๋ฆฐ์ด

0๊ฐœ์˜ ๋Œ“๊ธ€