All Articles

wecode 10주차_2일 TIL_공식문서와 함께하는 둘둘의 Next.js 도전기 2탄

Dynamic Pages

이번 챕터에서는 리액트에서처럼 마찬가지로 router 기능을 써볼건데 마찬가지로 Next.js에 기본적으로 내장되어 있는 기능이기 때문에 import만 해오면 된다!

공식문서 주소에 나온 clone해온 파일의 3번째 index.js를 다음과 같이 수정해준다.

import Layout from '../components/MyLayout';
import Link from 'next/link';

const PostLink = props => (
  <li>
    <Link href={`/post?title=${props.title}`}>
      <a>{props.title}</a>
    </Link>
  </li>
);
export default function Blog() {
  return (
    <Layout>
      <h1>My Blog</h1>
      <ul>
        <PostLink title="Hello Next.js" />
        <PostLink title="Learn Next.js is awesome" />
        <PostLink title="Deploy apps with Zeit" />
      </ul>
    </Layout>
  );
}

그러면 화면은 이렇게 돻!

image.png

이제 아래의 list에 있는 글씨들을 클릭했을때 화면 연결을 해주기 위해 post.js라는 파일을 하나 만든다.

import { useRouter } from "next/router";
import Layout from "../components/MyLayout";

const Page = () => {
    const router = useRouter();
    console.log(router, "콘솔")

    return(
        <Layout>
            <h1>{router.query.title}</h1>
            <p>This is the blog post content.</p>
        </Layout>
    )
}

export default Page;

여기서 router에 무슨 값이 들어가는지 궁금해서 콘솔을 찍어봤다.

image.png

내부에 다양한 값들이 존재하는데, 이 중에서 나는 query의 title을 갖다 써야 하므로! 그걸 써준다 ㅋㅋ 근데 신기한거 발견쓰!

image.png

걍 로컬 3000, 그니깐 메인에서 console을 찍었을 때의 query는 빈 객체지만 각각의 리스트에 있는 글자를 클릭하면 title 값이 객체 안에 들어가있다1 오오!!
성호쓰의 추측에 의하면.. Next.js는 SSR이고.. 페이지의 파일명이 곧 path가 되는거니깐 헤당 페이지에 접근했을 때 비로소 url이 바뀌고 그에 따라 빈 객체에 title이 생긴다는데 맞는말인것 같다 오오.. 천재쓰 ㅋㅋㅋㅋ

어찌됐든 useRouter를 통해 우리는 router 객체에 접근할 수 있으며, 이것은 바로 react HOOK과 같은 효과가 있다는 것!!!

Clean URLs with Dynamic Routing

이번엔 시작부터 좀 신기하다 파일이름이 [id].js라니.. 파일에 대괄호가 들어가도 된다니!!! 여튼 공식문서에 나와있는대로 열심히 쳐준다.

//[id].js
import { useRouter } from 'next/router';
import Layout from '../../components/MyLayout';

export default function Post() {
  const router = useRouter();

  return (
    <Layout>
      <h1>{router.query.id}</h1>
      <p>This is the blog post content.</p>
    </Layout>
  );
}

여튼 파일이름에 [] <- 이걸 써준 이상 동적으로 라우팅되게 할 수 있다고 한다.
괄호 안에 들어가는 id는 page에 의해 제공되는 query param의 이름이 된다고 한다! 아까 전엔 모두 한 페이지에 들어가게끔 라우팅을 구현했지만..
이제 각각 서로 다른 페이지에 들어갈 수 있도록 구현해볼 것이다.

// index.js
import Layout from '../components/MyLayout';
import Link from 'next/link';

const PostLink = props => (
  <li>
    <Link href="/p/[id]" as={`/p/${props.id}`}>
      <a>{props.id}</a>
    </Link>
  </li>
);

export default function Blog() {
  return (
    <Layout>
      <h1>My Blog</h1>
      <ul>
        <PostLink id="hello-nextjs" />
        <PostLink id="learn-nextjs" />
        <PostLink id="deploy-nextjs" />
      </ul>
    </Layout>
  );
}

다음과 같이 index.js 파일을 바꿔주고… 그 전에 짚고 넘어가야 할 부분!!
PostLink를 선언해준 부분을 잘 보면.. href는 경로를 나타내고, as는 url bar에 보이는 부분을 나타낸다고 한다.

as를 지워보면 우리의 url창은 http://localhost:3000/p/[id] 이렇게 화면 또한…

image.png

이상해 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

router가 어떻게 찍히는지 궁금하니까 또 콘손을 찍어보았다. 궁금쓰!

image.png

역시나 아까의 케이스처럼 메인 화면에서는 빈 객체였는데 리스트의 글자를 누르니 query에 id라는 key와 각 글자라는 value가 생겼다.
이게 바로 SSR MAGIC-★

image.png

화면은 아까와 똑같다!

image.png

url은 이렇게 찍히는데 나름 클린한건가?ㅋㅋㅋ
그 전꺼는 얼마나 드러웠는지 확인하려고 다시 3폴더로 이동하고 서버를 실행해보니..

image.png

진짜 드러운 url이었다 ㅋㅋㅋㅋㅋ

image.png

clean한 url을 만든 기념으로 clean한 이미지 투척!
내 블로그에서 출처를 안 쓰는 이미지들은 모두 pixabay에서 온 것들이다. 저작권 없는 무료이미지 최고 ㅋㅋ

Fetching Data for Pages

니꼴라스의 무비앱 만들기와 비슷하게 Next.js 공식문서에선 Batman Movie api를 받아오는 연습이 있다. 지금부터 스타투~~

먼저 명령어로 라이브러리를 깔아준다. yarn add isomorphic-unfetch 공식문서 index.js에 있는 모든 식을 따라치기 이전, 코드를 약간만 치고 console을 찍어보았다.

image.png

콘솔 잘 나오고있고.. 코드는 아래까지만 쳐봤다 일단

import Layout from '../components/MyLayout.js';
import Link from 'next/link';
import fetch from 'isomorphic-unfetch';

const Index = props => (
  <Layout>
    <h1>Batman TV Show</h1>
    <ul>
      {console.log(props, "프롭스")}
    </ul>
  </Layout>
)

Index.getInitialProps = async function() {
  const res = await fetch('https://api.tvmaze.com/search/shows?q=batman');
  const data = await res.json();
  console.log(`Show data fetched. Count: ${data.length}`)
  return {
    shows: data
  }
}

export default Index;

props로 들어가는 값을 비동기로 받아와서 json으로 변형한 후 data라고 선언해주는데 보통 fetch를 쓸 땐 .then으로 연결했던걸 이렇게 변수로 뙇!! 나타내니 더 보기가 편하네?!? 내가 이해하기론 getInitailProps가 then을 안 써도 fetch에서 바로 데이터를 변수에 넣고 사용할 수 있는듯..? 여튼…

이제 다시.. 남은 코드를 쳐보자.

import Layout from '../components/MyLayout.js';
import Link from 'next/link';
import fetch from 'isomorphic-unfetch';

const Index = props => (
  <Layout>
    <h1>Batman TV Show</h1>
    <ul>
      {props.shows.map(show => (
        <li key= {show.id}>
          <Link href="/p/[id]" as={`/p/${show.id}`}>
            <a>{show.name}</a>
          </Link>
        </li>
      ))}
    </ul>
  </Layout>
)

Index.getInitialProps = async function() {
  const res = await fetch('https://api.tvmaze.com/search/shows?q=batman');
  const data = await res.json();
  console.log(`Show data fetched. Count: ${data.length}`)
  return {
    shows: data.map(entry => entry.show)
  };
};

export default Index;

image.png

좋았어… 리스트대로 아주 잘 나오고 있다.
값이 잘 뜨는건 확인했고.. 새로고침을 한번 해보자.
그러고 터미널과 브라우저의 콘솔창을 확인해본다.

image.png

image.png

브라우저는 아주 깨-끗. 터미널에는 내가 공식문서에 입력된 콘솔처럼 data의 length인 10이 잘 찍힌다. 왜냐.. 서버에서 페이지를 랜더링 했기 때문이다. 우리는 이미 데이터를 가지고 있으니깐 client에서 다시 fetch를 쓸 이유는 없는것… 이라는데

아무래도 영 찝찝해서 다시 제대로 정리해보았다!

getInitialProps(줄여서 GIP)

  • 비동기 정적 메소드 (async static method)
  • 초기에 페이지가 로드될때 GIP은 오직 서버에서만 실행쓰
  • link Component나 routing API를 통해 다른 루트로 접근시 오직 client에서만 실행쓰
  • 무조건 객체 리턴쓰

궁금해서 찍어본 콘솔라시도~

Index.getInitialProps = async function() {
  const res = await fetch('https://api.tvmaze.com/search/shows?q=batman');
  console.log(res, "응답혀")
  const data = await res.json();
  console.log(data, "데이터여~")
  console.log(`Show data fetched. Count: ${data.length}`)
  return {
    shows: data.map(entry => entry.show)
  };
};

위의 코드에서 res와 data와 data의 length를 각각 찍어보았다. 먼저 res를 살펴보자

//res의 콘솔
Response {
  size: 0,
  timeout: 0,
  [Symbol(Body internals)]: {
    body: Gunzip {
      _writeState: [Uint32Array],
      _readableState: [ReadableState],
      readable: true,
      _events: [Object: null prototype],
      _eventsCount: 6,
      _maxListeners: undefined,
      _writableState: [WritableState],
      writable: false,
      allowHalfOpen: true,
      _transformState: [Object],
      _hadError: false,
      bytesWritten: 0,
      _handle: [Zlib],
      _outBuffer: <Buffer 5b 7b 22 73 63 6f 72 65 22 3a 32 34 2e 33 39 36 31 31 2c 22 73 68 6f 77 22 3a 7b 22 69 64 22 3a 39 37 35 2c 22 75 72 6c 22 3a 22 68 74 74 70 3a 2f 2f ... 16334 more bytes>,
      _outOffset: 0,
      _chunkSize: 16384,
      _defaultFlushFlag: 2,
      _finishFlushFlag: 2,
      _defaultFullFlushFlag: 3,
      _info: undefined,
      _level: -1,
      _strategy: 0
    },
    disturbed: false,
    error: null
  },
  [Symbol(Response internals)]: {
    url: 'https://api.tvmaze.com/search/shows?q=batman',
    status: 200,
    statusText: 'OK',
    headers: Headers { [Symbol(map)]: [Object: null prototype] },
    counter: 0
  }
} 응답혀

오.. 200 OK와 url이 젤 먼저 보인다 data도 함 까보자!

image.png

생각해보니 data.length가 10인데… 너무 기니까 걍 한 부분 캡쳐 고고!
이걸 console을 찍고 저장하면 바로 보이는 것이 아니라, 브라우저에서 새로고침을 해야 서버의 콘솔에 뜬다. 그리고 브라우저엔 아무 정보도 뜨지 않는다…

공식문서에 의하면 브라우저에선 Link Component 등으로 접근했을때 client에서만 실행된다고 했는데 진짜다 ㅋㅋㅋㅋ

image.png

아까까진 콘솔창이 넘나 깨-끗 했지만… 인제 데이터가 온다!!!!!
그니까 다시 정리해보자면…!
브라우저 단에서 뭔가를 띄우려면 url을 만지거나 Link 걸린 글자를 만지거나 해야하고 안그럼 다 서버단에서만 정보가 전송이 되는 것 같은 너낌적인 너낌…?!

styled-jsx

Next.js에서 CSS 효과를 주는 가장 좋은 방법은 styled-jsx인듯?
(보통은 material ui 같은 프레임워크를 많이 쓰는듯…?!)

image.png

사용전에 ctrl+shift+x를 눌러서 이 확장판을 깔면 코드치기 더 편할것이다 ㅋㅋ
아 배고파 꼬르륵 소리 나려고 하네 큰일났다.

index.js 파일을 이렇게 써준다.

import Layout from '../components/MyLayout';
import Link from 'next/link';

function getPosts() {
  return [
    { id: 'hello-nextjs', title: 'Hello Next.js' },
    { id: 'learn-nextjs', title: 'Learn Next.js is awesome' },
    { id: 'deploy-nextjs', title: 'Deploy apps with ZEIT' }
  ];
}

export default function Blog() {
  return (
    <Layout>
      <h1>My Blog</h1>
      <ul>
        {getPosts().map(post => (
          <li key={post.id}>
            <Link href="/p/[id]" as={`/p/${post.id}`}>
              <a>{post.title}</a>
            </Link>
          </li>
        ))}
      </ul>
      <style jsx>{`
        h1,
        a {
          font-family: 'Arial';
        }

        ul {
          padding: 0;
        }

        li {
          list-style: none;
          margin: 5px 0;
        }

        a {
          text-decoration: none;
          color: blue;
        }

        a:hover {
          opacity: 0.6;
        }
      `}</style>
    </Layout>
  );
}

저기서 getPost라는 함수를 대체 왜 써준진 모르겠다. 만든사람이 함수덕후?
여튼 styled 태그를 열고 중괄호와 빽틱을 또 열어서 css 요소들을 넣어주면 된다.
확장팩을 깔았으면 내 색깔! 안 깔았으면 주황색깔이 떠서 혼란혼란쓰~~ 확장팩 까세욧!

image.png

확장팩 까니까 을매나 보기 편하게요~

이제 Global Style도 적용해 볼건데, 그전에 react-markdown을 깔아주자 sudo yarn add react-markdown 명령어로 설치 고고! 코드를 한번 까봅시다~

import { useRouter } from 'next/router';
import Markdown from 'react-markdown';
import Layout from '../../components/MyLayout';

export default () => {
  const router = useRouter();
  return (
    <Layout>
      <h1>{router.query.id}</h1>
      <div className="markdown">
        <Markdown
          source={`
This is our blog post.
Yes. We can have a [link](/link).
And we can have a title as well.

### This is a title

And here's the content.
      `}
        />
      </div>
      <style jsx global>{`
        .markdown {
          font-family: 'Arial';
        }

        .markdown a {
          text-decoration: none;
          color: blue;
        }

        .markdown a:hover {
          opacity: 0.6;
        }

        .markdown h3 {
          margin: 0;
          padding: 0;
          text-transform: uppercase;
        }
      `}</style>
    </Layout>
  );
};

react-markdown을 깔아주었으므로 우리는 Markdown Component를 사용할 수 있고 그 컴포넌트 아래에선 마크다운으로 쓰면 된다 레알로 ㅋㅋ 씐기 씐ㄱ!!!
여태껏 reset.css를 만들거나.. global styles를 만들거나 했었는데
style jsx는 그냥 태그에 <style jsx global>만 붙이면 된다ㅋㅋ
근데 아직 내가 이게 미숙해서 그런지.. style jsx는 좀 불편해보인다 ㅠ

deploy는 다음기회에… 사실 한거 같긴한데 PORT 여러개 쓰는 부분이 잘 이해가 안가서 잠시 내려놓았다 ㅋㅋ