ํฌํธํด๋ฆฌ์ค ์ฌ์ดํธ๋ฅผ ๋ง๋ฌด๋ฆฌํ๋ฉด์ ๊ฐ์ฅ ๋ง์ง๋ง์ ์ถ๊ฐํ ๋ ๊ฐ์ง ๊ธฐ๋ฅ์ด ์๋ค.
๋ฐ๋ก ์ฐ๋ฝ์ฒ(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์ด๋ผ๋ ์ปค์คํ ํ ์ ๋ง๋ค์๋ค.
์ด ํ ์ 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๋ฅผ ํตํด ์ง์ ์ฐ๋ฝํ๊ธฐ ๋๋ฌธ. ์คํ๋ ค ํผ์ ๋ฒ๊ฑฐ๋ก์ธ ์ ์๋ค.
์คํฌ๋กค ๋ผ์ฐํ
๋์
์ด์ :
๋งํฌ ํด๋ฆญ ์ ์๊ฐ ์ด๋๋ณด๋ค ํจ์ฌ ์์ฐ์ค๋ฝ๊ณ , ์ฌ์ฉ์ ์์ ์ ํ๋ฆ์ ๋์ง ์์์ ์ข์๋ค.
์คํฌ๋กค ๊ฐ์ง ๊ธฐ๋ฅ ์ถ๊ฐ ์ด์ :
๋ด๊ฐ ์ด๋ค ์น์
์ ๋ณด๊ณ ์๋์ง ๋ช
ํํ๊ฒ ์ธ์งํ ์ ์์ด, ์ฌ์ฉ์ฑ๊ณผ ์ ๋ฌธ์ฑ์ด ํจ๊ป ์ฌ๋ผ๊ฐ๋ค.
์ด๋ฒ ์์
์ ์ฌ์ดํธ์ ์์ฑ๋๋ฅผ ๋์ด๋ ์ค์ํ ๋ง๋ฌด๋ฆฌ ๋จ๊ณ์๋ค.
ํนํ ์ฌ์ฉ์๊ฐ ๋ง์ง๋ง๊น์ง ์ฌ์ดํธ๋ฅผ ๋ดค์ ๋, ์ฐ๋ฝํ ์ ์๋ ๋ฐฉ๋ฒ์ด ๋ช
ํํ๊ฒ ์๋ด๋์ด์ผ ์ง์ง ํฌํธํด๋ฆฌ์ค๋ผ๊ณ ์๊ฐํ๋ค.
๋ํ, ์คํฌ๋กค ๋ผ์ฐํ
๊ณผ ๊ฐ์ง ๊ธฐ๋ฅ์ ์์ํ์ง๋ง ์ฌ์ฉ์ ๊ฒฝํ์ ํ ๋์ด์ฌ๋ฆฌ๋ ํต์ฌ ํฌ์ธํธ์๋ค.