작성자는 본래 노션을 이용해 포트폴리오 페이지를 제작하였습니다. 노션은 깔끔하고 직관적인 UI를 제공하고 여러 종류의 블록을 조립하는 형태로 페이지를 만들기 때문에 이를 통해 단기간 안에 준수한 디자인의 포트폴리오 웹페이지를 제작할 수 있었습니다.
그러나 노션에서 제공하는 블록의 디자인이 제한적이고, 특히 포트폴리오 페이지를 만들기 위한 역할과는 거리가 먼 도구라고 생각했습니다. 이에 고심 끝에 포트폴리오 페이지를 직접 제작하여 조금 더 작성자가 추구하는 디자인과 구성으로 포트폴리오 페이지를 구축하고자 하였습니다.
본 글에 서는 포트폴리오 페이지 제작 중 웹페이지 방문자가 처음 접하는 HomepageHeader
컴포넌트와 docusaurus swizzle
을 통한 Custom NavbarLayout
을 구성하는 과정을 기록하고자 합니다.
본 포트폴리오 페이지는 Docusaurus를 이용하여 제작하였습니다. Docusaurus는 React 기반의 정적 웹사이트 생성기로, React나 Markdown을 이용하여 페이지를 구성할 수 있습니다.
Docusaurus는 Infima라는 디자인 프레임워크에 기반하는데, Light/Dark 모드, Responsive Layout을 굉장히 깔끔하게 지원하고, 디자인적인 측면에서도 제공하는 컴포넌트나 색상 팔레트가 불필요하게 화려한 부분도 없고 매우 훌륭하다고 생각합니다. 아직 Infima는 개발 단계이지만, 개인적으로 지금까지 활용해본 디자인 프레임워크 중 매우 모던한 디자인을 매우 편리하게 구현할 수 있는 성장 가능성이 매우 높은 프레임워크라고 생각합니다.
Idea
작성자는 풍경 사진을 좋아합니다. 특히 도시의 야경 사진을 좋아하고, 멋진 야경을 보면 동기 부여가 되기도 하고, 단순히 바라보는 것만으로도 마음 깊숙한 곳에서 벅차오르는 느낌을 받기도 합니다. 그리고 이러한 느낌을 포트폴리오 페이지에도 담고자 했습니다.
구체적으로, 아래의 결과물처럼 포트폴리오 페이지에 처음 접속했을 때, 모니터가 일종의 창문처럼 작용하여 멋진 도시의 풍경을 보여주고, 이를 통해 방문자가 조금이나마 제가 추구하는 분위기를 느낄 수 있도록 하고자 했습니다.
![]() | ![]() |
---|---|
HomepageHeader : Light Mode | HomepageHeader : Dark Mode |
Implementation
이러한 HomepageHeader
섹션을 구성하는 데에 작성자는 아래와 같은 요소들을 고려하였습니다.
Seamless Light/Dark Mode Support
Infima를 사용하는 Docusaurus는 기본적으로 Light Mode와 Dark Mode를 지원합니다. 이에 자연스럽게 포트폴리오 페이지를 개발할 적에도 이 부분을 신경쓰게 되었습니다.
그 중에서 사용자가 웹페이지에 처음 방문했을 시 보게 되는 HomepageHeader
섹션에서는 위에 첨부한 사진처럼 사용자가 일종의 창문을 바라보는 느낌으로 멋진 도시의 풍경을 보여주고자 했습니다.
작성자는 HomepageHeader
의 컨셉은 "창문"이기 때문에 건물에 바퀴가 달려 통째로 움직이는 것이 아닌 이상 Light Mode와 Dark Mode에서 보여지는 풍경이 같아야 하고, 이 부분이 매우 중요하다 생각했습니다.
이에 Unsplash에서 사진을 촬영한 위치가 같고 시간만 다른 사진을 찾는데 시간을 투자했습니다. 그리고 결국 2시간 정도의 시간을 들여 위에 첨부한 사진처럼 Light Mode와 Dark Mode에서 보여지는 풍경이 같은 사진을 찾을 수 있었습니다.
Newyork: Daytime
: https://unsplash.com/ko/%EC%82%AC%EC%A7%84/eKSv2czdiaMNewyork: Nighttime
: https://unsplash.com/ko/%EC%82%AC%EC%A7%84/KIx_5ReSgIo
위와 같이 찾은 사진으로 HomepageHero
컴포넌트를 구성하고, 이를 이용해 HomepageHeader
를 구성하였습니다.
const HomepageHero: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
return (
<header className={clsx("hero hero--primary", styles.hero)}>
{children}
</header>
);
};
HomepageHeader
const HomepageHeader: React.FC = () => {
const { siteConfig } = useDocusaurusContext();
const personal = siteConfig.customFields.personal as Record<string, string>;
return (
<HomepageHero>
<div className="container">
<h1 className={clsx("hero__title", styles.text, styles.title)}>
{siteConfig.tagline}
</h1>
<p className={clsx("hero__subtitle", styles.text, styles.subtitle)}>
{personal.tagline}
</p>
<div className={styles.buttonWrapper}>
<Link
className="button button--secondary button--lg"
to="/docs/category/%EC%9D%B4%EB%A0%A5%EC%84%9C"
>
{translate({
id: "pages.Home.Header.button.text",
message: "이력서 내려받기",
})}
</Link>
</div>
</div>
</HomepageHero>
);
};
이미지는 [data-theme="dark"]
특성 선택자를 이용해 Theme Mode에 따라 다르게 보여지도록 하였고, transition
CSS 프로퍼티를 활용해 자연스러운 전환 효과를 구현하였습니다.
--ifm-*
의 형태를 가진 CSS 변수는 Infima에서 제공하는 CSS 변수입니다.
.hero {
/* ... properties */
background-position: center;
background-size: cover;
background-image: url("/img/newyork-light.jpg");
[data-theme="dark"] & {
background-image: url("/img/newyork-dark.jpg");
}
transition: background-image var(--ifm-transition-fast) var(
--ifm-transition-timing-default
);
}
NavbarLayout
swizzling
@docusaurus/theme-classic
에서 기본으로 제공하는 네비게이션 바는 기본적으로 아래와 같은 구성을 가지고 있습니다.
Docusaurus의 Full Implementation은 여기를 참조하세요.
function Navbar(): JSX.Element {
return (
<NavbarLayout>
<NavbarContent />
</NavbarLayout>
);
}
여기서 <NavbarContent />
컴포넌트는 바로가기 버튼, Light/Dark Mode Toggle 등 네비게이션 바에 포함되는 컴포넌트들을 구성하는 역할을 하고, <NavbarLayout />
은 이러한 컴포넌트의 Container 역할을 합니다.
작성자는 HomepageHeader
의 컨셉이 "창문"이기 때문에 네비게이션 바의 컨텐츠만 HomepageHeader
의 사진 위에 떠있는 것처럼 구현하고 싶었습니다. 그러나 기본으로 제공하는 <NavbarLayout />
은 항상 정해진 background-color
를 가지고 있기 때문에 이를 구현하기 위해서는 작성자의 유스케이스에 맞게 <NavbarLayout />
을 Swizzle 해야 했습니다.
이를 위해 작성자는 먼저 docusaurus swizzle
명령어를 이용해 NavbarLayout
을 따로 구성할 수 있도록 하였습니다.
그리고 <NavbarLayout />
과 이와 관련한 컴포넌트 및 훅, 유틸리티 함수를 살펴보기 시작했습니다.
NavbarLayout
의 기본 구성
Docusaurus에서 제공하는 기본적인 <NavbarLayout />
의 구성은 아래와 같습니다.
function NavbarLayout({ children }: Props): JSX.Element {
const {
navbar: { hideOnScroll, style },
} = useThemeConfig();
const mobileSidebar = useNavbarMobileSidebar();
const { navbarRef, isNavbarVisible } = useHideableNavbar(hideOnScroll);
return (
<nav
ref={navbarRef}
aria-label={translate({
id: "theme.NavBar.navAriaLabel",
message: "Main",
description: "The ARIA label for the main navigation",
})}
className={clsx(
"navbar",
"navbar--fixed-top",
hideOnScroll && [
styles.navbarHideable,
!isNavbarVisible && styles.navbarHidden,
],
{
"navbar--dark": style === "dark",
"navbar--primary": style === "primary",
"navbar-sidebar--show": mobileSidebar.shown,
},
)}
>
{children}
<NavbarBackdrop onClick={mobileSidebar.toggle} />
<NavbarMobileSidebar />
</nav>
);
}
position: fixed
먼저 className