Don't think! Just do it!

종합 IT 기술 정체성 카오스 블로그! 이... 이곳은 어디지?

Next.js

Next.JS 프로젝트 기본 템플릿 만들기#3

방피터 2022. 6. 16. 14:14

이것 저것 하려다 보니 기본적으로 들어가야 하는 것들이 더 필요한 것 같아.

 

1. 기본 Layout 설정해두는 거랑

2. Mui appbar랑 footer정도 만들어서 스토리북에 넣어두고(나중에 지우더라도)

3. storybook에 mui addon이 있네? 그것도 설치해볼거야. 해봤는데 뭐가 문제인지 잘 안되는 거 같더라구 ㅎㅎ

 

next.js하려면 기본적으로 layout이 있어야겠지? 그치? 아닌가? ㅎㅎ 몰라 난 처음해보는 거라 ㅎ 그런데 뭐 세상에 정답이 어디있겠어? ㅋㅋㅋ next.js 문서 정독했는데 졸면서 읽었으니 다 까먹었겠지? ㅎ 그래서 layout 파트를 다시 읽어봤어.

next.js 문서에서 소개하는 layout 방식은 두 가지가 있네. 하나는 _app.tsx에서 header footer같은 컴포넌트 다이렉트로 때려박는 방법. 그럼 전체 page가 단일 layout을 사용하게 되겠지. 다른 하나는 getLayout 이라는 property를 넣어서 page별로 layout을 지정하는 방법이 있지. 웹 페이지가 단일 layout으로 구성되는 경우는... -_- 있나? 로그인 페이지에 메뉴랑 사이드 바랑 그런거 있으면 이상하잖아? ㅋ 아니라고 생각하는 사람은 간편하게 첫번째 방식으로 고고

 

암튼 이 글에서는 페이지별로 layout을 구현하도록 할거야. 우선 _app.tsx를 변경해야해.

import * as React from "react";
import Head from "next/head";
import { AppProps } from "next/app";
import { ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import { CacheProvider, EmotionCache } from "@emotion/react";
import theme from "../src/theme";
import createEmotionCache from "../src/createEmotionCache";
import { NextPage } from "next";
// import GlobalStyles from "./../styles/GlobalStyles";

// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();

interface MyAppProps extends AppProps {
  emotionCache?: EmotionCache;
  Component: NextPageWithLayout;
}

/*For Per-page layout! */
type NextPageWithLayout = NextPage & {
  getLayout?: (page: React.ReactElement) => React.ReactNode;
};

export default function MyApp(props: MyAppProps) {
  const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
  const getLayout = Component.getLayout ?? ((page) => page);
  return getLayout(
    <CacheProvider value={emotionCache}>
      <Head>
        <meta name="viewport" content="initial-scale=1, width=device-width" />
      </Head>
      <ThemeProvider theme={theme}>
        {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
        <CssBaseline />
        {/* <GlobalStyles /> */}
        <Component {...pageProps} />
      </ThemeProvider>
    </CacheProvider>
  );
}

특별한 건 없어. typescript사용하니까 getlayout 함수를 사용하기 위한 type을 좀 만들어 주고 getlayout함수로 전체 컴포넌트를 감싸 주는거지.  아래 한 부분만 보면 page component에 layout이 있으면 그걸 쓰시구요. 아니면 page를 그대로 리턴해주세요~ 라고 되어 있잖아? 이제 각각 page component에 getLayout을 정의해 주면 되겠네.

 const getLayout = Component.getLayout ?? ((page) => page);

 

자 이제 index.tsx에서 어떻게 하는지 보자구. 쓸데없는 것들 좀 정리했고 getServerSideProps는 그대로 놔뒀어 ㅎㅎ 맨날 까먹으니까~ 익숙해지면 템플릿에서 지우도록 하자고 ㅎㅎ

import type { NextPage } from "next";
import Head from "next/head";
import { Container } from "@mui/material";
import tw, { styled } from "twin.macro";
import React, { ReactElement, ReactNode } from "react";
import Layout from "../components/Layout";

export const getServerSideProps = async () => {
  const res = await fetch("http://localhost:3000/api/hello");
  const data = await res.json();
  if (!data) {
    return {
      notFound: true,
    };
  }
  return {
    props: data,
  };
};

type NextPageWithLayout<T> = NextPage<T> & {
  getLayout?: (page: ReactElement) => ReactNode;
};

const MainContainer = styled(Container)(() => [tw`text-center`]);

const Home: NextPageWithLayout<{ name: string }> = ({ name }) => {
  return (
    <MainContainer>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main>
        <h1>안녕 여러분!!</h1>
      </main>
    </MainContainer>
  );
};
export default Home;

Home.getLayout = function getLayout(page: ReactElement) {
  return <Layout>{page}</Layout>;
};

그리고 _app.tsx하고 마찬가지로 getLayout 추가한 type을 새로 만들었고 (이거 여러 페이지에서 사용할테니까 다른 파일로 빼내야겠다.) 그리고 맨 마지막에 getLayout 설정하고 page에 맞는 layout으로 감싸서 페이지를 리턴하면 되지. 이제 앞으로 모든 페이지는 이런 페턴으로 만들어주면 돼.

 

자 그럼 이제 간단한 <Layout>을 하나 만들어 보자구.👇 우선 components폴더에 파일을 총 3개 만들었어.

layout components

Layout.tsx

import React from "react";
import MainAppBar from "./MainAppBar";
import MainFooter from "./MainFooter";

const Layout: React.FC<any> = ({ children }) => {
  return (
    <>
      <MainAppBar />
      {children}
      <MainFooter />
    </>
  );
};

export default Layout;

 

그야말로 Layout 컴포넌트. MUI의 appbar랑 기존 next.js footer를 활용해서 MainAppBar랑 MainFooter를 만들고 위 아래로children(페이지)를 감싸주는 형태로 했어. MainAppBar랑 MainFooter는 github 참고하자 너무 길어~ 이렇게 하고 나면 아래와 같은 결과~ 요런식으로 해주면 이제 슬슬 뭔가 보인다 그치?

per-page layout 적용 결과


자 협업을 컴포넌트 드리븐!! 스토리북에도 넣자고. 세개 전부 다. MainAppbar랑 MainFooter 그리고 Layout까지! 우선 기존 example 싹 다 지워주고 stories/components 폴더에 스토리 파일 3개 추가하자구~  다 해봤잖아? ㅎㅎㅎ

스토리 폴더~

Layout.stories.tsx, MainAppBar.stories.tsx, 그리고 MainFooter.stories.tsx 만들고 각각 내용을 채워주자구~ MyButton.stories.tsx 만들어 둔거 참고하면 쉽잖아? MainAppBar.stories.tsx 하나만 보자구.(나머지는 git 참고)

import { ComponentStory } from "@storybook/react";
import MainAppBar from "../../components/MainAppBar";

export default {
  title: "Components/AppBar",
  component: MainAppBar,
};

const Template: ComponentStory<typeof MainAppBar> = (args) => (
  <MainAppBar {...args} />
);
export const Default = Template.bind({});
Default.args = {};

뭐 설명할게 없다 그치? 스토리북 하라는 데로 하는겨 나도 ㅎㅎ 암튼 이렇게 하면 스토리북 브라우저에서 아래와 같이 추가된 걸 볼 수 있지👇👇

storybook components

스토리북 Layout 모습도 잘 보이고 말이지.

자 그런데 Pages에서 Home을 보면 이런게 하나도 없어. Layout이 하나도 설정이 안된거지. 스토리북에는 _app.js 이런게 적용이 안되니까 말이야. 그럼 여기에는 _app.js에서 콜되는 getLayout을 어떻게 불러오지?

Layout 적용이 안되어 있다.

몰라 ㅋ 그래서 내 맘대로 할거야. storybook 파일에서 아래처럼 무식하게 감싸줘도 되는데. Layout이 page 별로 변경되거나 다른 레이아웃을 사용하면 스토리북도 같이 바꿔야 하잖아? 언제 그렇게 해 귀찮게.

   <Layout>
     <Home {...args} name={name} />
   </Layout>

그래서 아래처럼 home.stories.tsx를 변경했어

import { ComponentStory } from "@storybook/react";
import Home, { getServerSideProps } from "../../pages/index";

export default {
  title: "Pages/Home",
  component: Home,
};

export const HomePage: ComponentStory<typeof Home> = (
  args,
  { loaded: { name } }
) => {
  const getLayout = Home.getLayout ?? ((page) => page);
  return getLayout(<Home {...args} name={name} />);
};

HomePage.args = { name: "John Doe" }; //default args
HomePage.loaders = [
  async () => {
    let data = await getServerSideProps();
    return data.props;
  },
];

_app에서 하는 기능 그대로 따온거야. getLayout 불러와서 있으면 그거 반환 없으면 그냥 페이지 반환. 이렇게하면 원해 index.tsx에 있는 home.getLayout에 있는 layout 그대로 스토리북에 표현되겠지. 요렇게 말이야. 👇👇👇

layout이 적용된 storybook page

그럼 스토리북도 얼추 끝났네. 한 가지 해결 못한 점만 빼면 말야. 왜 사파리 스토리북에서는 조선 글씨체가 나오지? 아 킹받아. 이건 따로 해결해보자. 아니네 사파리에서는 다 이러네 아.. 킹...

사파리 브라우저에서 스토리북 조선글씨체

추가) 사파리에서 정상적으로 폰트가 로드되지 못하는 이유는 두 가지야. 첫번째는 _app.tsx에서 해주는 cssBaseLine이 storybook에서는 없어서 그렇고 두 번째는 _document.tsx에서 구글 폰트를 import하기 때문에 storybook에서는 적용이 안되지. 게다가 사파리에는 내장 폰트도 없거든. 이 두가지 문제 모두 스토리북의 preview.js에서 처리해주면 되는데 <cssBaseLine />도 삽입하고 폰트도 로드해주자. 아래와 같이 말이지👇👇👇

//preview.js
import CssBaseline from "@mui/material/CssBaseline";
...
export const decorators = [
  (Story) => (
    <>
      <link
        rel="stylesheet"
        href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
      />
      <CssBaseline />
      <Story />
    </>
  ),
];
...

일부 소스 코드중에 font-family를 monospace로 해놓게 있는데 이것도 사파리 브라우저에 없는 폰트. 마찬가지로 사용하려면 웹폰트로 삽입해야만 하겠지? 아님 바꾸던가 난 Roboto로 변경함.

 

아래는 github repository 주소! 👇👇👇

https://github.com/peter-bang/nextjs-basic-configuration

 

GitHub - peter-bang/nextjs-basic-configuration: nextjs basic configurations

nextjs basic configurations. Contribute to peter-bang/nextjs-basic-configuration development by creating an account on GitHub.

github.com


이제 마지막으로 storybook-addon-material-ui 라는 걸 설치해볼게. 이건 옵션이야 해도 그만 안해도 그만. 스토리북 addon중 하나인데 업데이트가 1년 전이니까 알아서들 하자.

뭐 설명으로는 mui 컴포넌트 개발을 돕는 애라고 하는데 역시 지능이 낮은 나는 똥인지 된장인지 찍어먹어봐야 알지.

해봤는데 안되네 ㅋㅋㅋ 걍 하지 말자!


다들 안녕~

반응형