본문 바로가기
Project

개인 포트폴리오 제작 회고

by KMS_99 2024. 3. 13.

 

프로젝트 명: 개인 포트폴리오

기간: 2023.12 ~ 지속

사용기술: TypeScript, Next.js, recoil, react-query, tailwind, supabase, swiper, framer

site: https://www.kimmyungsub-portfolio.com/

 

FE 김명섭 포트폴리오

어서오세요. 웹 프론트엔드 개발자 김명섭입니다.

www.kimmyungsub-portfolio.com

github: https://github.com/kms99/portfolio

 

GitHub - kms99/portfolio

Contribute to kms99/portfolio development by creating an account on GitHub.

github.com

 

 

내일배움캠프를 진행하면서 진행이 무뎌졌던 개인 포트폴리오를 최근 진행하였다.

기술적 의사결정 내용 트러블 슈팅, 이후 진행할 개발 예정 내용을 공유하려고 한다.


기술적 의사결정

1. Next.js

이번 프로젝트는 Next.js를 사용하였다.

 

사실 프로젝트에서 라우팅을 사용하지 않지만 Next.js에서는 두가지 라우팅 방식이 있다.

pages router와 app router의 선택지가 있었으며, 라우팅 방식 외에도 각 라우팅 방식 별 파일구조나 코드 사용에 대한 차이가 존재하였다. 이번 프로젝트에서는 app router를 사용하였다. 이유는 이전 Pixtudy 프로젝트에서 pages router를 사용하기도 했으며, 최신기술인 app router를 익힐 수 있었기 때문이다.

 

이 프로젝트에서 Next.js를 선택한 이유는 무엇보다도 폰트 및 이미지, seo 최적화 때문이다.

프레임워크에서 간편하게 지원하는 각종 최적화 방식은 Next.js를 선택할 충분한 이유였다.

 

2. tailwind

styled-components의 개발방식이 익숙하지만 이번 프로젝트에서는 tailwind css를 사용하였다.

이유는 Next.js 프레임워크를 사용하면서, 가장 권장하는 css 방식이기 때문이다.

 

가장 권장하는 방식이기 때문에 다른 추가적인 설정이나 보일러플레이트 없이 개발이 가능하다.

styled-components는 SSR과 SSG에서 사용하기 위해서 보일러플레이트를 작성 해야하는 단점이 있다.

 

현재 tailwind css를 통해 개발을 진행하였지만 tailwind css의 최대의 단점인 가독성이 떨어지는 상태이다.

tailwind css를 좀 더 가독성 사용하는 방식을 고민하고 리펙토링 할 예정이다. (TODO)

 

3. supabase

사실 최초 프로젝트를 진행할 때 정적으로 페이지를 구성하여 로딩이 적게 발생하여 사용자 UX를 개선하고자 하였다.

supabase를 사용하게 된 이유는 보여줘야 할 데이터의 양 때문이다.

 

진행한 프로젝트의 내용을 모달을 통해서 보여주는데 정적 데이터로 진행하기엔 내용이 너무 많았다.

 

따라서 supabase를 사용하여 데이터 테이브을 구성하였고, 프로젝트 정보를 불러오고 있다.

 

4. recoil

최초 about section에 있는 book의 상태 관리를 위해 contextAPI를 사용하였다.
book 부분에서만 상태관리가 필요했기 때문에 contextAPI로 충분히 사용가능 했다.

 

개발을 진행하면서, 프로젝트 내용을 modal로 보여주는 것으로 결정하였고 modal을 구성할 때 상태관리 필요했다.

따라서 상태관리 요소가 많아지면서 contextAPI를 사용하는 것보다는 다른 상태관리 툴을 사용하는 것이 좋다고 판단했다.

 

recoil 라이브러리를 채택한 이유는 useState와 비슷한 형태로 사용하기 때문에 더 리액티브한 개발을 경험할 수 있으며, redux와 redux-toolkit, zustand는 경험해보았기에 새로운 기술을 학습하고 싶었다.

 


트러블 슈팅

1. header의 nav버튼 클릭 시 주소창에 id가 노출되는 문제

 

본 프로젝트에서는 페이지 내부에서 스크롤 이벤트를 통한 section 인덱싱이 필요하였다.
기존에는 Link href에 원하는 위치의 id값을 입력하면서 스크롤을 구현하였다.

import Link from 'next/link';
import React from 'react';
import { NavItemsType } from '../layout.type';

interface Props {
  nav: NavItemsType;
}

export default function HeaderNavItem({ nav }: Props) {
  return (
    <li className="[&:hover]:text-white [&+li]:ml-3">
      <Link href={`#${nav.id.split('-')[1]}`} className="w-full h-full p-3 [&:hover]:bg-gray-700 font-main font-light">
        {nav.text}
      </Link>
    </li>
  );
}

 

위 코드는 기존코드로 두가지 문제를 확인할 수 있다.

1. 주소창에 #about, #project 와 같이 파라미터가 추가되는 문제가 발생하였다.

2. next.js에서 라우팅을 지원하는 Link 컴포넌트를 id를 통한 스크롤이벤트 인덱싱으로 사용하기에 부적절하다.

 

특히 1번 문제는 사용자가 id 정보를 확인 할 수 있을 뿐더러 #about 페이지일 때 다른 section으로 스크롤 해도 #about 파라미터가 유지되는 문제가 발생한다.

 

두가지 문제를 다음 코드를 통해 해결하였다.

 

import React from 'react';
import { NavItemsType } from '../layout.type';

interface Props {
  nav: NavItemsType;
}

export default function HeaderNavItem({ nav }: Props) {
  const scrollToElement = (e: React.MouseEvent<HTMLAnchorElement>, id: string) => {
    e.preventDefault();
    const element = document.getElementById(id);
    if (element) {
      element.scrollIntoView({ behavior: 'smooth', block: 'start' });
    }
  };

  return (
    <li className="[&:hover]:text-white [&+li]:ml-3">
      <a
        href={`#${nav.id.split('-')[1]}`}
        onClick={e => scrollToElement(e, nav.id.split('-')[1])}
        className="w-full h-full p-3 [&:hover]:bg-gray-700 font-main font-light"
      >
        {nav.text}
      </a>
    </li>
  );
}

 

주요 변경사항이다.

  1. Link->a 태그 변경
  2. onClick이벤트 추가
    1. e.preventDefault()를 통해 기본 이벤트 제거
    2. document.getElementById를 통해 이동할 DOM 요소에 접근
    3. 해당 DOM 요소의 scrollIntoView를 통해 화면전환

위 변경사항을 통해 주소창에 id가 노출되지 않도록 개선하였다.

 

 

2. Next.js app router에서 next/head의 Head태그가 작동하지 않는 문제

 

Next.js에서는 seo 최적화를 위해 Head 태그를 지원한다.

Head 태그에는 title, metadata 등 seo를 돕는 여러 설정을 부여할 수 있다.

 

최초 Head 태그를 통해서 다음과 같이 코드를 작성하여 사용하였다.

import Head from 'next/head';
import React from 'react';

export default function OpenGraph() {
  return (
    <Head>
      <title>FE 김명섭 포트폴리오</title>
      <meta name="description" content="어서오세요. 웹 프론트엔드 개발자 김명섭입니다." />
      <meta name="keywords" content="웹프론트엔드, 포트폴리오, 웹개발자, 프론트엔드" />
      <meta name="author" content="Kim MyungSub" />

      {/* Facebook */}
      <meta property="og:url" content="https://kimmyungsub-portfolio.com" />
      <meta property="og:type" content="website" />
      <meta property="og:site_name" content="FE 김명섭 포트폴리오" />
      <meta property="og:title" content="FE 김명섭 포트폴리오" />
      <meta property="og:description" content="어서오세요. 웹 프론트엔드 개발자 김명섭입니다." />
      <meta
        property="og:image"
        content="https://jeaukpjgukscmujtxqot.supabase.co/storage/v1/object/public/seo/seo.png"
      />
    </Head>
  );
}

 

 

import './globals.css';
import localFont from 'next/font/local';
import Header from './components/layout/header/Header';
import RecoilRootWrapper from './states/recoil/RecoilRootWrapper';
import TanstackQueryWrapper from './states/tanstackQuery/TanstackQueryWrapper';
import Footer from './components/layout/footer/Footer';
import OpenGraph from './OpenGraph';

const pretendard = localFont({
  src: [
    { path: './font/Pretendard-Bold.ttf', style: 'bold' },
    { path: './font/Pretendard-Medium.ttf', style: 'light' },
    { path: './font/Pretendard-Light.ttf', style: 'medium' },
  ],
  display: 'swap',
  variable: '--font-pretendard',
});

const sb = localFont({
  src: [
    { path: './font/SB 어그로 B.ttf', style: 'bold' },
    { path: './font/SB 어그로 L.ttf', style: 'light' },
    { path: './font/SB 어그로 M.ttf', style: 'medium' },
  ],
  display: 'swap',
  variable: '--font-sb',
});

const patua = localFont({ src: './font/PatuaOne-Regular.ttf', display: 'swap', variable: '--font-patua' });

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ko" className={`${pretendard.variable} ${sb.variable} ${patua.variable}`}>
      <body>
        <OpenGraph />
        <TanstackQueryWrapper>
          <RecoilRootWrapper>
            <Header />
            {children}
            <Footer />
            <div id="modal-portal" />
          </RecoilRootWrapper>
        </TanstackQueryWrapper>
      </body>
    </html>
  );
}

 

이 코드를 통해 기대 했던 바는 seo최적화 및 openGraph 설정이다.

하지만 코드는 기대했던 대로 동작하지 않았고 이를 해결하기 위해 검색을 진행하였다.

 

https://nextjs.org/docs/app/building-your-application/optimizing/metadata#static-metadata

 

Optimizing: Metadata | Next.js

Use the Metadata API to define metadata in any layout or page.

nextjs.org

 

관련한 공식문서를 발견하였으며, Next.js의 app router에서는 Head태그를 지원하지 않는다는 것을 알 수 있었다.

대신 metadata 객체를 export 하면서 seo를 최적화 할 수 있었다.

 

따라서 다음과 같이 코드를 변경하였다.

 

import './globals.css';
import localFont from 'next/font/local';
import { Metadata } from 'next';
import Header from './components/layout/header/Header';
import RecoilRootWrapper from './states/recoil/RecoilRootWrapper';
import TanstackQueryWrapper from './states/tanstackQuery/TanstackQueryWrapper';
import Footer from './components/layout/footer/Footer';

const pretendard = localFont({
  src: [
    { path: './font/Pretendard-Bold.ttf', style: 'bold' },
    { path: './font/Pretendard-Medium.ttf', style: 'light' },
    { path: './font/Pretendard-Light.ttf', style: 'medium' },
  ],
  display: 'swap',
  variable: '--font-pretendard',
});

const sb = localFont({
  src: [
    { path: './font/SB 어그로 B.ttf', style: 'bold' },
    { path: './font/SB 어그로 L.ttf', style: 'light' },
    { path: './font/SB 어그로 M.ttf', style: 'medium' },
  ],
  display: 'swap',
  variable: '--font-sb',
});

const patua = localFont({ src: './font/PatuaOne-Regular.ttf', display: 'swap', variable: '--font-patua' });

export const metadata: Metadata = {
  title: 'FE 김명섭 포트폴리오',
  description: '어서오세요. 웹 프론트엔드 개발자 김명섭입니다.',
  authors: [{ name: 'Kim MyungSub', url: 'https://kimmyungsub-portfolio.com' }],
  keywords: '웹프론트엔드, 포트폴리오, 웹개발자, 프론트엔드',
  openGraph: {
    type: 'website',
    url: 'https://kimmyungsub-portfolio.com',
    siteName: 'FE 김명섭 포트폴리오',
    title: 'FE 김명섭 포트폴리오',
    description: '어서오세요. 웹 프론트엔드 개발자 김명섭입니다.',
  },
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ko" className={`${pretendard.variable} ${sb.variable} ${patua.variable}`}>
      <body>
        <TanstackQueryWrapper>
          <RecoilRootWrapper>
            <Header />
            {children}
            <Footer />
            <div id="modal-portal" />
          </RecoilRootWrapper>
        </TanstackQueryWrapper>
      </body>
    </html>
  );
}

 

해당 문제를 해결하면서 다시한번 공식문서의 힘을 느낄 수 있었다. 


이후 진행 예정 사항

 

개발을 진행하였지만 추가로 개선하고 발전시킬 부분이 많다.

 

1. 반응형 디자인 구현

반응형 디자인을 구현하고자 시도를 하였지만 about section의 book의 구현에 대한 고민으로 보류상태에 있다.

book의 애니메이션을 구현하고자 하면 크기가 작아져 UX적인 부분이 떨어질 것이라고 생각된다.

해당 부분은 고민을 통해서 1순위로 개발할 예정이다.

 

2. tailwind 개선

현재 tailwind를 사용하여 개발하면서 코드 가독성이 매우 떨어지며, 동적 크기 설정과 같은 더 효율적인 코드처리가 미숙한 상태이다. 이러한 부분을 개선하여 코드 가독성과 재사용성을 극대화 할 생각이다.

 

3. 스켈레톤 ui 구현

supabase를 통해 데이터를 불러오기 때문에 불러오는 시간동안에 로딩처리를 통한 UX 개선이 필요하다.

loading progress 처리도 아주 좋지만 요즘 스켈레톤 ui를 통한 로딩처리를 통해 더 자연스러운 처리도 가능하다고 들었다.

따라서 우선순위는 낮지만 스켈레톤 ui 처리를 통해 UX를 개선시킬 생각이다.

 

3. framer를 이용한 애니메이션 추가

본 프로젝트에서는 framer를 사용하고 있다.

다만 애니메이션 처리보다는 scroll 시 motion.div를 통해서 viewport에 위치했는지 여부만 판단하고 있다.

framer를 통해 다양한 애니메이션을 경험해 보고 싶기 때문에 추후 추가 예정이다.