๊ตญ๋น ๋ถํธ ์บ ํ ์๋ฃ ํ ์ทจ์ ์ ์ํด ํฌํธํด๋ฆฌ์ค๋ฅผ ๋ง๋ค์์ต๋๋ค.
์ฌ์ฉํ ๊ธฐ์ ๊ณผ ๊ธฐ๋ฅ๋ค์ ์ ๋ฆฌํด๋ณด์์ต๋๋ค.
โญ๏ธ๋ฐฐํฌ ๋งํฌ: https://portfolio-hellohailie.vercel.app/
๐์ฌ์ฉํ ๊ธฐ์
- Next.js : ์ฒ์ ๋ฐฉ๋ฌธํ์ ๋ ๋น ๋ฅด๊ฒ ๋ก๋ฉ์ด ๋๋ฉด์, ํฌํธํด๋ฆฌ์ค๊ฐ ๊ฒ์์ด ์ ๋๋ฉด ์ข๊ฒ ๋ค๋ ์๊ฐ์ Next.js๋ฅผ ์ฌ์ฉํ์์ต๋๋ค.
- tailwind CSS : ์ฝ๊ฒ ๋ฐ์ํ ์น์ ๋ง๋ค๊ณ ์ ํ๊ธฐ ๋๋ฌธ์ tailwind CSS๋ฅผ ์ฌ์ฉํ์ต๋๋ค. class์ ์ค์ ์ ํ๋ฉด ๋๊ธฐ ๋๋ฌธ์ ํ์ผ์ ๋ฐ๋ก ์ค์ ํ ํ์๋ ์๊ณ , ํ๋ฉด์ ์,์๋๋ก ๋ฒ๊ฐ์๊ฐ๋ฉด์ ๊ฐ๋ฐํ์ง ์์๋ ๋์ ์ข์์ต๋๋ค.
๐กgetStaticProps ํจ์๋ฅผ ํ์ฉํ Notion API ์ฐ๋
Nextjs์ ๊ฒฝ์ฐ ์ฌ์ ๋ ๋๋ง(pre-rendering)์ getServerSideProps, getStaticProps๋ผ๋ ํจ์๋ฅผ ์ฌ์ฉํด์ ์ฝ๊ฒ ๊ตฌํํ ์ ์์ต๋๋ค. getStaticProps๋ ์ต์ด ๋น๋ ์์ ๋ฑ ํ ๋ฒ๋ง ํธ์ถ์ด ๋ฉ๋๋ค. ์ฆ, ์ต์ด ๋น๋ ์ ๋น๋๋๋ ๊ฐ์ด ์ถํ์ ์์ ๋ ์ผ์ด ์๋ ๊ฒฝ์ฐ์ ์ฌ์ฉํ๊ธฐ ์ข๋ค๋ ๊ฒ์ธ๋ฐ ์ ์ ์ธ ํ์ด์ง๋ฅผ ๋ ๋๋งํ๊ธฐ ์ข์ต๋๋ค.
์ ๋ Notion์ ์ ๋ฆฌํด๋ ํ๋ก์ ํธ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๊ธฐ๋ง ํ๋ฉด ๋๊ธฐ ๋๋ฌธ์ ๋ฐ์ดํฐ์ ์ค์๊ฐ ์ฑ์ด ์ค์ํ์ง ์์์ต๋๋ค. ๊ทธ๋์ getServerSideProps๊ฐ ์๋ getStaticProps๋ฅผ ์ฌ์ฉํ๊ฒ ๋์์ต๋๋ค.
import Layout from "../components/layout";
import Head from "next/head";
import { TOKEN, DATABASE_ID } from "../config";
import axios from "axios";
import ProjectItem from "../components/projects/projectItem";
export default function Projects({ projects }) {
return (
<Layout>
<div className='flex flex-col items-center justify-center min-h-screen mb-10 px-10'>
<Head>
<title>์ด์ ๋ฏผ ํฌํธํด๋ฆฌ์ค</title>
<meta name='description' content='์ด์ ๋ฏผ ํฌํธํด๋ฆฌ์ค' />
<link rel='icon' href='/favicon.ico' />
</Head>
<h1 className='text-4xl sm:text-6xl py-5 flex justify-center '>
<span className='italic'>I have </span>
<span className='text-violet-600 font-bold italic'>
{projects.results.length}
</span>
<span className='italic'> projects</span>
</h1>
<div className='grid grid-cols-1 lg:grid-cols-3 gap-8 m-6 py-10 w-full'>
{projects.results.map((aProject) => (
<ProjectItem data={aProject} key={aProject.id} />
))}
</div>
</div>
</Layout>
);
}
export async function getStaticProps() {
const options = {
method: "POST",
url: `https://api.notion.com/v1/databases/${DATABASE_ID}/query`,
headers: {
accept: "application/json",
"Notion-Version": "2022-02-22",
"content-type": "application/json",
authorization: `Bearer ${TOKEN}`,
},
data: JSON.stringify({
sorts: [
{
property: "order",
direction: "descending",
},
],
page_size: 100,
}),
};
const res = await axios.request(options);
const projects = await res.data;
return {
props: { projects }, // will be passed to the page component as props
};
}
๐ก๋ฐ๋ณต๋๋ ์ฝ๋ ์ค์ด๊ธฐ
๊ธฐ์ ์คํ ํ์ด์ง๋ ๋ฐ๋ณต๋๋ ์ฝ๋๊ฐ ๋ง์์ต๋๋ค. ๊ทธ๋์ ๊ธฐ์ ์คํ ์ค๋ช ๋ถ๋ถ์ ๋ฐ๋ก ๋นผ์ ๋ฐฐ์ด๋ก ๋ง๋ค๊ณ , ์ด ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์์ map ๊ณ ์ฐจํจ์๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ๋ณต๋๋ ์ฝ๋๋ฅผ ์ค์ผ ์ ์์์ต๋๋ค.
// skill-item.js
const frontSkillsItem = [
[
<React key={React} />,
"React",
"๋ค์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ ์ ์๋ React๋ฅผ ์ฃผ๋ก ์ฌ์ฉํฉ๋๋ค.",
],
[
<Javascript key={Javascript} />,
"Javascript",
"ECMAScript์ ๋ณํ๋ฅผ ๊พธ์คํ ์ดํผ๊ณ ํ์ตํฉ๋๋ค.",
],
...
// skill.js
<div className='flex flex-wrap lg:w-3/5 md:mx-auto sm:mb-2'>
{frontSkillsItem.map((a, idx) => (
<div className='p-2 xl:w-1/2 w-full relative' key={idx}>
<div className='skill-text'>
{a[0]}
<span className='title-font font-medium text-xl ml-4 dark:text-black'>
{a[1]}
</span>
</div>
<div className='skill-cover'>
<h1 className='title-font font-medium text-xl m-4 dark:text-black'>
{a[0]}
</h1>
<p className='leading-relaxed ml-3 p-0 dark:text-black'>{a[2]}</p>
</div>
</div>
))}
</div>
๐ก๋คํฌ ๋ชจ๋
- next-themes ํ์ฉ
- Avoid Hydration Mismatch :
์๋ฒ์์ ํ ๋ง๋ฅผ ์ ์ ์๊ธฐ ๋๋ฌธ์ useTheme์์ ๋ฐํ๋๋ ๊ฐ์ ํด๋ผ์ด์ธํธ์ ๋ง์ดํธ๋ ๋๊น์ง ์ ์๋์ง ์์ต๋๋ค. ์ฆ, ํด๋ผ์ด์ธํธ์ ๋ง์ดํธ๋๊ธฐ ์ ์ ํ์ฌ ํ ๋ง๋ฅผ ๊ธฐ๋ฐ์ผ๋ก UI๋ฅผ ๋ ๋๋งํ๋ ค๊ณ ํ๋ฉด Hydration ๋ถ์ผ์น ์ค๋ฅ๊ฐ ํ์๋ฉ๋๋ค.
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์ ํ์ด์ง๊ฐ ํด๋ผ์ด์ธํธ์ ๋ง์ดํธ๋ ๋ ํ์ฌ ํ ๋ง๋ฅผ ์ฌ์ฉํ๋ UI๋ง ๋ ๋๋ง๋๋๋ก useEffect๋ฅผ ์ฌ์ฉํ์ต๋๋ค. useEffect๋ ํด๋ผ์ด์ธํธ์์๋ง ์คํ๋๋ฏ๋ก ์์ ํ๊ฒ UI๋ฅผ ํ์ํ ์ ์์ต๋๋ค.
๐ฅHydrate๋?
Server Side ๋จ์์ ๋ ๋๋ง ๋ ์ ์ ํ์ด์ง์ ๋ฒ๋ค๋ง๋ JSํ์ผ์ ํด๋ผ์ด์ธํธ์๊ฒ ๋ณด๋ธ ๋ค, ํด๋ผ์ด์ธํธ ๋จ์์
HTML ์ฝ๋์ React์ธ JS์ฝ๋๋ฅผ ์๋ก ๋งค์นญ ์ํค๋ ๊ณผ์
์ ๋งํ๋ค.
import { useTheme } from "next-themes";
import { useState, useEffect } from "react";
export default function DarkModeBtn() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
// useEffect only runs on the client, so now we can safely show the UI
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return (
<>
<button
className='inline-flex items-center bg-gray-100 border-0 py-1 px-3 focus:outline-none hover:bg-gray-100 text-orange-400 dark:text-yellow-500 dark:bg-slate-600 rounded text-base mt-4 md:mt-0'
type='button'
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
>
<svg
width='16'
height='16'
fill='currentColor'
className='dark:invisible visible h-5 w-5 dark:h-0 dark:w-0'
viewBox='0 0 16 16'
xmlns='http://www.w3.org/2000/svg'
>
<path d='M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z' />
</svg>
<svg
width='16'
height='16'
fill='currentColor'
className='invisible dark:visible dark:h-5 dark:w-5 h-0 w-0'
viewBox='0 0 16 16'
xmlns='http://www.w3.org/2000/svg'
>
<path d='M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z' />{" "}
<path d='M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z' />
</svg>
</button>
</>
);
}
๐กClass๋ฅผ ์ฌ์ฉํ์ฌ ํ ๋ง ์ค์
- Next.js ์ฑ์ด ํด๋์ค๋ฅผ ์ฌ์ฉํ์ฌ ํ ๋ง๋ฅผ ์ค์ ํ ์ ์๋๋ก ์์ฑ์ class๋ก ๋ณ๊ฒฝํ์ฌ ์ฌ์ฉํ์ต๋๋ค.
// _app.js
import "../styles/globals.css";
import { ThemeProvider } from "next-themes";
function MyApp({ Component, pageProps }) {
return (
<ThemeProvider attribute='class'>
<Component {...pageProps} />
</ThemeProvider>
);
}
export default MyApp;
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
mode: "jit",
content: ["./pages/**/*.{js,jsx}", "./components/**/*.{js,jsx}"],
darkMode: "class",
theme: {
extend: {},
},
plugins: [],
};
๐ก๋ฐ์ํ ์น
- tailwind CSS๋ฅผ ์ฌ์ฉํ์ฌ ๋ ์ด์์์ ๋๋ฐ์ด์ค ๋ณ๋ก ๋ณ๊ฒฝํ์์ต๋๋ค. (sm, md, lg, xl)
<section className='text-gray-600 body-font'>
<div className='container mx-auto flex px-5 py-24 items-center justify-center flex-col gap-5'>
<div>
<h1 className='title-font sm:text-4xl text-3xl mb-4 font-medium text-gray-900'>
์๋
ํ์ธ์ ๊ฐ๋ฐ์ ์ด์ ๋ฏผ์
๋๋ค
</h1>
...
๐ ์๋ชป๋ ๊ฐ๋ ์ ๋ฌ์ด ์๋ค๋ฉด ๋๊ธ ๋ถํ๋๋ฆฝ๋๋ค. ์ ์ ์ฑ์ฅ์ ํฐ ๋์์ด ๋ฉ๋๋ค๐ค
'๐ฅธReview > ํ๋ก์ ํธ' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
ํ๋ฆฌ์จ๋ณด๋ฉ ํ์ ์คํฌ๋ฆฝํธ + ๋ฆฌ์กํธ์ฟผ๋ฆฌ ๊ณผ์ refactoring ์ผ์ง (0) | 2023.01.13 |
---|---|
PuppyBuddy ํ๋ก์ ํธ ํ๊ณ & ๋ถํธ์บ ํ ์๋ฃ ํ๊ธฐ (0) | 2022.10.19 |