๐ŸฅธReview/ํ”„๋กœ์ ํŠธ

๊ฐœ์ธ ํฌํŠธํด๋ฆฌ์˜ค ํ”„๋กœ์ ํŠธ ํ›„๊ธฐ

hellohailie 2022. 11. 17. 23:35

 

๊ตญ๋น„ ๋ถ€ํŠธ ์บ ํ”„ ์ˆ˜๋ฃŒ ํ›„ ์ทจ์—…์„ ์œ„ํ•ด ํฌํŠธํด๋ฆฌ์˜ค๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. 

์‚ฌ์šฉํ•œ ๊ธฐ์ˆ ๊ณผ ๊ธฐ๋Šฅ๋“ค์„ ์ •๋ฆฌํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค. 

 

 

โญ๏ธ๋ฐฐํฌ ๋งํฌ: 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&nbsp;</span>

          <span className='text-violet-600 font-bold italic'>
            {projects.results.length}
          </span>
          <span className='italic'>&nbsp;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>
            ...

 

 

 

๐Ÿ˜ƒ ์ž˜๋ชป๋œ ๊ฐœ๋… ์ „๋‹ฌ์ด ์žˆ๋‹ค๋ฉด ๋Œ“๊ธ€ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ์ €์˜ ์„ฑ์žฅ์— ํฐ ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค๐Ÿค“