포트폴리오를 노션 링크로 제출하려니 경력사항과 프로젝트 목록이 한 눈에 들어오지 않는 점이 아쉬워서 웹사이트를 만들게 되었다.
막상 포트폴리오 사이트를 만들려고 하니 디자인을 어떻게 해야 할지 막막했다.
그래서 "프론트엔드 개발자 이력서" 키워드로 검색해서 다른 분들의 디자인을 많이 참고했다.
그리고 Velog 사이트와 네이버, 카카오 등의 기업 소개 사이트들도 디자인을 구상할 때 많은 도움이 되었다.
회사 경력 사항, 프로젝트 상세 내용과 같은 부분은 들어갈 내용이 많고, 업데이트를 할 일이 잦다보니 아무래도 마크다운으로 관리하는 것이 편할 것 같았다.
그래서 아래 사진처럼 노란색 박스로 표시한 부분에 마크다운을 임베딩하는 방식으로 구현했다.
마크다운을 넣는 데에는 react-markdown
라이브러리를 사용했다.
그리고 마크다운 디자인을 커스텀하기 위해 Material UI의 styled
함수를 사용하여 코드 블록, 하이퍼링크 등의 CSS를 정의한 StyledMarkdown
컴포넌트를 만들었다.
import { styled } from "@mui/material";
const StyledMarkdown = styled("div")(({ theme }) => {
return {
"h1, h2, h3": {
marginTop: 30,
},
// Code Block
"& pre": {
padding: "0px 10px",
borderRadius: 2,
},
// ... 중략
};
});
export default StyledMarkdown;
그리고 아래와 같이 ReactMarkdown
컴포넌트를 StyledMarkdown
컴포넌트로 감싸주었다.
그리고 마크다운에 HTML 코드를 넣을 수 있도록 rehypePlugins
에 rehype-raw
라이브러리를 추가해주었다.
export default function CustomMarkdown({ mdFilePath }: CustomMarkdownProps) {
// ... 중략
return (
<StyledMarkdown>
<ReactMarkdown components={components} rehypePlugins={[rehypeRaw]}>
{markdown}
</ReactMarkdown>
</StyledMarkdown>
);
}
노트북 화면을 기준으로 한 행에 그리드 아이템이 3개인 게 가장 보기가 좋았다.
교육 및 대외활동 섹션이나, 자격증 및 수상 섹션은 마침 딱 3개가 들어가서 딱이다 싶었는데, 문제는 프로젝트 섹션에 4개 항목이 들어가야 했다.
네 번째 항목만 덩그러니 다음 행으로 떨어져 있으면 보기가 안 좋았고, 그렇다고 "더보기" 버튼을 넣자니 새로운 페이지를 만들어야 해서 마땅찮았다.
그래서 고민하다가 Carousel을 넣는 것이 좋겠다고 결론을 내렸다.
Carousel 라이브러리로는 Swiper
, Slick Slider
, react-material-ui-carousel
를 설치해서 비교해봤는데, Swiper
에 내가 원하는 기능이 모두 들어가 있어서 이 라이브러리로 구현했다.
Swiper
는 이전/다음 버튼을 커스텀할 수 있고, 반응형으로 슬라이드 개수를 지정할 수 있어서 좋았다.
"use client";
import { Container, IconButton } from "@mui/material";
import { Swiper } from "swiper/react";
import { Navigation } from "swiper/modules";
import NavigateBeforeIcon from "@mui/icons-material/NavigateBefore";
import NavigateNextIcon from "@mui/icons-material/NavigateNext";
import { ReactNode } from "react";
import "./Carousel.css";
interface Props {
id: string;
children: ReactNode;
}
export default function CarouselContainer({ id, children }: Props) {
return (
<>
<Container sx={{ maxWidth: "80%" }}>
<Swiper
cssMode={true}
modules={[Navigation]}
slidesPerView={3}
spaceBetween={24}
navigation={{
prevEl: `#${id}-prev-slide-button`,
nextEl: `#${id}-next-slide-button`,
disabledClass: "swiper-button-disabled",
}}
pagination={true}
breakpoints={{
0: {
slidesPerView: 1,
},
920: {
slidesPerView: 2,
},
1200: {
slidesPerView: 3,
},
}}
style={{
width: "100%",
height: "100%",
}}
>
{children}
</Swiper>
</Container>
<IconButton
id={`${id}-prev-slide-button`}
sx={{ position: "absolute", top: "46%", left: "4vw", zIndex: 1000 }}
>
<NavigateBeforeIcon fontSize="large" />
</IconButton>
<IconButton
id={`${id}-next-slide-button`}
sx={{ position: "absolute", top: "46%", right: "4vw", zIndex: 1000 }}
>
<NavigateNextIcon fontSize="large" />
</IconButton>
</>
);
}
이번에 포트폴리오 사이트를 만들면서 신경 쓴 점은 어떤 디바이스, 플랫폼에서 접속하더라도 최대한 일관적인 디자인을 보여드릴 수 있도록 하는 것이었다.
먼저 다양한 브라우저에서 일관된 모양이 나올 수 있도록 reset css를 적용하는 작업을 진행했다.
Material UI의 경우 <CssBaseline />
컴포넌트를 통해 이 작업이 가능했다.
나는 앱 최상단 layout.tsx
에 <CssBaseline />
를 추가해주었다.
기본적으로 media query를 사용해서 뷰포트의 크기에 따라 화면이 다르게 보일 수 있도록 구현했다.
그런데 서버 사이드 렌더링 컴포넌트의 경우 렌더링 시점에 뷰포트의 크기를 알 수 없어서 문제가 발생했다.
모바일에서 접속하면 PC용 화면이 잠시 보였다가 렌더링이 완료된 이후에 모바일용 화면이 보이는 문제가 있었다.
이 문제는 Next.js의 Middleware를 사용해서 해결할 수 있었다.
아래 코드와 같이 Next.js의 middleware.ts
에서 User-Agent
정보를 분석해서 렌더링이 이루어지기 전에 모바일 접속 여부를 파악할 수 있도록 했다.
그리고 이 정보를 response 헤더에 담아서 컴포넌트에 전달했다.
디바이스 타입 정보를 편하게 읽을 수 있도록 useViewport
훅을 만들어서 사용했다.
export function middleware(request: NextRequest) {
const { device } = userAgent(request);
const viewport = device.type === "mobile" ? "mobile" : "desktop";
const response = NextResponse.next();
response.headers.set("viewport", viewport);
return response;
}
import { headers } from "next/headers";
export const useViewport = () => {
const viewport = headers().get("viewport");
return {
isMobile: viewport === "mobile",
isDesktop: viewport === "desktop",
};
};
미쳤다..