웹/Next.js

Next.js 포트폴리오 만들기 회고

공대생철이 2022. 6. 30. 21:54
728x90

이 글은 '개발하는 정대리' 님의 Next.js 포트폴리오 사이트를 듣고 실습한 후에 작성하는 회고록입니다.

https://www.youtube.com/watch?v=KvoFvmu5eRo&t=5717s

사용된 스택

- Next js

- tailwind css

- Notion API

- LottieFiles (그림 삽입)

 

구현 목표

1. 홈페이지

- LottieFiles에서 Lottie 삽입하기

- Tailwind 라이브러리에서 헤더와 푸터 가져오기

- 포트폴리오 페이지와 링크 연결

- 헤더와 푸터에서 홈페이지 링크 연결

 

2. 프로젝트 페이지

- Notion에서 데이터 테이블 생성한 후 fetch로 데이터 가져오기

- 가져온 데이터를 컴포넌트 하나를 map하는 구조로 화면 생성

- 컴포넌트에 삽입할 데이터

 -> 제목

 -> 설명

 -> 깃허브 주소 (Link로 만들기)

 -> 사용한 기술스택

 -> 작업기간 (Date 함수 활용하여 계산)

 

3. 다크 모드

- Next-themes 라이브러리 활용하여 dark: 일 때 css 코드 별개로 구성 

 

구현 과정

1. 홈페이지

페이지와 컴포넌트 구성은 다음과 같이 했다.

홈페이지는 index.js에서 구성했다.

		<Layout>
			<Head>
				<title>Park SC Portfolio</title>
				<meta
					name='description'
					content='오늘도 빡코딩
        '
				/>
				<link rel='icon' href='/favicon.ico' />
			</Head>
			<section className='flex min-h-screen flex-col items-center justify-center text-gray-600 body-font'>
				<div className='container mx-auto flex px-5 py-24 md:flex-row flex-col items-center'>
					<Hero />
				</div>
			</section>
		</Layout>

 

		<div className='bg-primary'>
			<Header />
			<div>{children}</div>
			<Footer />
		</div>

레이아웃에 Header와 Footer를 넣어주고 children을 받아서 넣어주는 형식으로 구성

 

Layout 태그로 전체를 감싸고 body section에 해당하는 부분만 index.js에서 만들어주었다.

tailwind css를 활용하여 className에 css 속성들이 모두 들어가있어 코드가 조금 길다.

 

export default function Hero() {
	return (
		<>
			<div className='lg:flex-grow md:w-1/2 lg:pr-24 md:pr-16 flex flex-col md:items-start md:text-left mb-16 md:mb-0 items-center text-center'>
				<h1 className='title-font sm:text-3xl text-3xl mb-4 font-medium text-gray-900'>
					안녕하세요 땡땡땡입니다.
					<br className='hidden lg:inline-block' />
					오늘도 빡코딩
				</h1>
				<p className='mb-8 leading-relaxed'>
					대중을 생명을 끓는 사람은 없는 미묘한 봄바람이다. 천자만홍이 이상이
					살았으며, 이것을 품으며, 없으면 피가 온갖 봄바람이다. 물방아 스며들어
					우리는 가슴에 부패를 뜨거운지라, 얼마나 칼이다. 뛰노는 끝까지 우리의
					인생에 쓸쓸한 피다. 생의 따뜻한 귀는 인간의 구하지 있다. 몸이 희망의
					찾아다녀도, 사랑의 봄바람이다. 능히 투명하되 인간이 그것을 것은 돋고,
					이상의 이것이다. 커다란 보내는 얼마나 철환하였는가? 따뜻한 트고,
					그들은 것이 얼마나 위하여서. 맺어, 있는 되는 어디 얼음이 가슴에
					교향악이다.
				</p>
				<div className='flex justify-center'>
					<Link href='/projects'>
						<a className='btn-project'>프로젝트 보러 가기</a>
					</Link>
				</div>
			</div>
			<div className='lg:max-w-lg lg:w-full md:w-1/2 w-5/6'>
				<Animation />
			</div>
		</>
	);
}

Hero 태그는 다음과 같이 구성

 

h1 태그로 큰 제목 하나 

p 태그로 본문 내용 (한글 입숨에서 가져옴) 

프로젝트 보러가기 버튼 (button 태그 말고 a 태그 + Link 활용)

LottieFile 넣어줄 Animation 컴포넌트

import animation from "../../public/animation.json";

export default function Animation() {
	return (
		<Lottie
			loop
			animationData={animation}
			play
		/>
	);
}

Lottie 그냥 홈페이지 가서 json 코드 따와서 패키지 설치에서 넣어주기만 하면 끝

https://lottiefiles.com/

(이렇게 간단한 걸 몰랐다니)

구현한 화면은 다음과 같다.

캡처해서 오른쪽 Lottie가 가만히 있는데 사실 계속 움직이는 중임.

 

나머지 Link 작업은 Next에서 제공해주는 Link를 활용하였다.

Next의 개꿀 장점 중 하나인 라우팅이 편하다는 것을 이용해서 page에 있는 애들을 그냥 Link href로 불러오면 끝

					<Link href='/projects'>
						<a className='btn-project'>프로젝트 보러 가기</a>
					</Link>

이런 식으로 하면 바로 localhost:3000/projects로 이동하고 pages 폴더에 있는 projects.js를 불러온다.

 

2. 프로젝트 페이지

 

결과를 먼저 보자.

Header랑 Footer는 똑같이 가져갈 거니깐 Layout으로 감싸고

카드 형식의 컴포넌트 ProjectItem을 만들어서 데이터를 불러오고 map으로 돌릴 생각을 하면 된다.

 

Notion API가 익숙치 않아서 좀 애먹긴 했는데 생각보다 설명이 되게 친절하게 나와있어서 할만했다.

먼저 노션에서 연습할 데이터를 만들어준다. (노션을 DB로 쓸 생각은 1도 못했음) 

하나의 아이템에는 사진 + 다음과 같은 데이터들이 담겨져 있음. 

이제 얘를 불러오는 게 관건.

 

Notion API 공식문서로 가보자. 

https://developers.notion.com/reference/retrieve-a-database

 

Retrieve a database

Connect Notion pages and databases to the tools you use every day, creating powerful workflows.

developers.notion.com

Notion 데이터베이스를 GET하는 방법이 나와있다.

홈페이지 들어가서 오른쪽에 있는 코드 그대로 복붙하고 몆 줄만 추가해줬다.

export async function getStaticProps() {
	const options = {
		method: "POST",
		headers: {
			Accept: "application/json",
			"Notion-Version": "2022-02-22",
			"Content-Type": "application/json",
			Authorization: `Bearer ${TOKEN}`,
		},
		body: JSON.stringify({
			sorts: [
				{
					property: "name",
					direction: "ascending",
				},
			],
			page_size: 100,
		}),
	};

	const response = await fetch(
		`https://api.notion.com/v1/databases/${DATABASE__ID}/query`,
		options
	);

	const projects = await response.json();

	return {
		props: { projects }, // will be passed to the page component as props
	};
}

TOKEN이랑 DATABASE_ID는 노션 회원가입하면 내 정보에서 볼 수 있으니 각자 꺼 쓰면 된다.

(env 파일로 따로 빼놓았음)

 

여기서 Next.js에서 데이터를 가져오는 방법 두 가지가 등장한다.

1. getServerSideProps

If you export a function called getServerSideProps (Server-Side Rendering) from a page, Next.js will pre-render this page on each request using the data returned by getServerSideProps.

export async function getServerSideProps(context) {
  return {
    props: {}, // will be passed to the page component as props
  }
}

공식문서 설명을 가져온 것인데  getServerSideProps는 data를 사용하는 각 request에 대해서 화면을 pre-render한다.

얘는 서버 사이드 렌더링(SSR)이고 브라우저에서 작동하는 애가 아니다. 

 

2. getStaticProps

If you export a function called getStaticProps (Static Site Generation) from a page, Next.js will pre-render this page at build time using the props returned by getStaticProps.

export async function getStaticProps(context) {
  return {
    props: {}, // will be passed to the page component as props
  }
}

얘는 데이터(props)를 사용하면서 build time에 pre-render를 한다. 페이지를 build하는 과정에서 데이터를 가져와서 페이지에 렌더링하는 구조이다. 

 

두 과정을 간단하게 설명하면 

link나 라우터로 API request를 서버에 보냄 -> 서버에서 데이터를 가져옴 -> 그 데이터를 가지고 페이지를 렌더링

 

둘다 서버 사이드 렌더링인데 뭐가 다른 것이냐.

 

getServerSideProps는 데이터가 바뀔 때마다 페이지를 계속 렌더링하는 것이다. 그러니깐 데이터 변경 유무에 따라서 페이지가 작동하는 원리를 가진다. 데이터 의존적인 fetch 방식.

 

getStaticProps는 페이지를 build할 때 불러오고 그 다음에는 데이터와 상관없다. 데이터 변경이 일어나도 페이지는 변하지 않고 변경된 데이터를 불러오고 싶으면 build 과정을(ex. 새로고침) 새로 해줘야 한다.

 

우리는 Notion에 있는 정적인 데이터를 가지고 페이지를 만드는 것이기에 getStaticProps를 활용한다.

 

async await를 사용하여 데이터를 가져올 때까지 기다리고 이제 projects라는 props으로 데이터를 보내줄 수 있다.

 

데이터 가져오는 거랑 활용하는 부분이랑 같은 페이지에 뒀습니다.

export default function Projects({ projects }) {
	console.log(projects);

	return (
		<Layout>
			<div className='flex flex-col items-center justify-center min-h-screen px-6  mb-10'>
				<Head>
					<title>Park SC Portfolio</title>
					<meta
						name='description'
						content='오늘도 빡코딩
        '
					/>
					<link rel='icon' href='/favicon.ico' />
				</Head>
				<h1 className='text-4xl font-bold sm:text-6xl'>
					{" "}
					총 프로젝트 :
					<span className='pl-4 text-blue-500'> {projects.results.length}</span>
				</h1>
				<div className='grid grid-cols-1 md:grid-cols-2 py-6 m-6 gap-6 '>
					{projects.results.map((project) => {
						return <ProjectItem key={project.id} data={project}></ProjectItem>;
					})}
				</div>
			</div>
		</Layout>
	);
}

getStaticProps에서 보낸 projects를 받아서

총 프로젝트 수랑 각 project에 대해서 map 돌려서 Item 만드는 거로 구성했다.

 

export default function ProjectItem({ data }) {
	const Title = data.properties.name.title[0]?.plain_text;
	const Github = data.properties.Github.url;
	const ProjectStartDate = data.properties.WorkPeriod.date.start;
	const ProjectEndDate = data.properties.WorkPeriod.date.end;
	const Description = data.properties.Description.rich_text[0].plain_text;
	const ImageSrc = data.cover.external?.url;
	const tags = data.properties.tag.multi_select;

	const calculatedPeriod = (start, end) => {
		const calcMilliseconds =
			new Date(end).getTime() - new Date(start).getTime();
		return Math.round(calcMilliseconds / (1000 * 60 * 60 * 24));
	};

	return (
		<div className='project-card'>
			<Image
				className='rounded-t-xl'
				src={ImageSrc}
				layout='responsive'
				objectFit='cover'
				quality={100}
				alt='picture'
				width='100%'
				height='60%'
			/>

			<div className='p-4 flex flex-col'>
				<h1 className='text-2xl font-semi-bold'>{Title}</h1>
				<h3 className='mt-4 text-xl'>{Description}</h3>
				<a href={`//${Github}`} target='blank'>
					깃허브 바로가기
				</a>
				<div className='flex items-start mt-2'>
					{tags.map((aTag) => {
						return (
							<h1
								className='px-2 py-1 mr-2 rounded-md bg-sky-200 dark:bg-sky-700 w-30'
								key={aTag.id}>
								{aTag.name}
							</h1>
						);
					})}
				</div>
				<p className='my-1'>{`작업기간 : ${ProjectStartDate} ~ ${ProjectEndDate} (${calculatedPeriod(
					ProjectStartDate,
					ProjectEndDate
				)}일) `}</p>
				<p>{``}</p>
			</div>
		</div>
	);
}

아이템을 만드는 컴포넌트의 구성이다.

뭐가 길어보이지만 별거 없다.

원하는 데이터를 파싱해서 넣어준 것뿐이다.

여기서 활용한 데이터는

- Image 소스

- 제목

- 설명 

- Github 주소

(이게 그냥 href를 쓰면 localhost:3000/github~ 이렇게 감)

(대신 //를 넣어주면 그냥 새로운 주소로 불러와서 연결됨)

- 프로젝트 시작기간, 종료기간 

(총 며칠인지 계산하는 함수 만들어서 적용)

 

3. 다크 모드

next-themes 패키지 하나면 끝난다.

import { useTheme } from "next-themes";

export default function DarkModeToggleButton() {
	const { theme, setTheme } = useTheme();

	return (
		<>
			<button
				className='inline-flex items-center 
                bg-gray-100 border-0 py-1 px-3 
                focus:outline-none
                rounded-full
                hover:bg-gray-100 
                hover:text-orange-500
                dark:text-slate-300
                dark:bg-slate-600
                dark:hover:text-yellow-500
                '
				type='button'
				onClick={() => {
					setTheme(theme === "dark" ? "light" : "dark");
				}}>
				{theme === "dark" ? (
					<svg
						xmlns='http://www.w3.org/2000/svg'
						className=' h-5 w-5'
						viewBox='0 0 20 20'
						fill='currentColor'>
						<path d='M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z' />
					</svg>
				) : (
					<svg
						xmlns='http://www.w3.org/2000/svg'
						className=' h-5 w-5'
						viewBox='0 0 20 20'
						fill='currentColor'>
						<path
							fillRule='evenodd'
							d='M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z'
							clipRule='evenodd'
						/>
					</svg>
				)}
			</button>
		</>
	);
}

리액트 훅 쓰듯이 useTheme이라는 훅을 사용하면 된다.

버튼을 하나 만들어서 light 테마인지 dark 테마인지 toggle하게 만들었다.

 

그러고는 css 가서 dark일 때 어떤 css 할 지 설정하면 끝.

.bg-primary {
	@apply bg-white dark:bg-slate-800;
}

이런 식으로 평소에는 배경화면을 white로 하고 dark 일 때는 배경화면을 까만색으로 바꾸면 된다.

글자 color나 그림자 색깔, border 색깔 같은 것도 알아서 바꿔주면 됨.

 

구현한 화면은 위의 동영상과 같은 느낌이다.

 

어려움을 느꼈던 점

- Notion API를 활용하는 것이 아무래도 처음이다 보니 익숙하지 않아서 에러가 많았다.

- GET, POST 등의 CRUD 시스템에 대해서 알고 있다고 생각했는데 막상 실제로 구현을 하려고 보니 기본 개념이 좀 흔들리는 것 같아서 이해하는데 시간이 좀 걸렸다. (지금도 완벽히 이해했다고 생각하지 않아 추가적인 공부 필요성을 느낌.)

- env 파일을 다루는 것을 많이 안 해봐서 어떻게 따로 빼고 정리해야 되는지 감을 아예 못 잡았음.

- tailwind css에서 margin이나 padding이 어느정도를 차지하는 지 감을 잡지 못해 초기에는 좀 어려웠지만 할수록 익숙해졌음.

 

배웠던 점

- Lottie라는 게 이렇게 쉽게 되는 건지 몰랐다. 나중에 진짜 내 포트폴리오를 만들때도 활용해보면 좋을듯.

- Next에서 data를 어떻게 가져오는지 알게 되었다. 예전에 nodejs 처음 배울 때 프론트로 가져올 때 되게 낑낑거렸던 느낌인데 Next를 활용한 이번 경우 훨씬 간단하게 진행된 것 같은 느낌. 이게 프레임워크의 힘인가 싶다.

- 이상하게 계속 삼항연산자가 헷갈렸는데 어디가 true일 때고 어디가 false일 때인지 확실히 알게 되었음. 

- tailwind css가 bootstrap보다 뭔가 더 세련된 느낌이고 좀 더 기능이 많은 느낌이었다. 확실히 bootstrap보다 좀 더 세밀한 조작이 가능해서 꽤 괜찮은 css 라이브러리인 듯.

 

 

 

 

728x90

' > Next.js' 카테고리의 다른 글

Nextjs - Focus 되는 NavBar 만들기  (0) 2023.05.06