Next JS tutorial

홍왕열·2023년 9월 11일
0

TIL

목록 보기
53/56

설치

npx create-next-app@latest .

일단 node가 설치가 되어있어야 한다.
그렇다면 위와 같이 명령어를 하면 되는데, @latest는 최신 버전을 사용한다는 뜻이고 .은 현재 폴더에 만든다는 뜻이다.
그 뒤 여러가지 질문들이 나오는데, 나는 일단 생코를 보면서 연습해 볼 것이기 때문에 아래와 같이 하였다

✔ Would you like to use TypeScript? … No
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like to use src/ directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the default import alias? … No

맨 아래는 아직은 잘 모르겠다. 검색해서 찾아봐야할 듯 하다.
여기서 중요한 것은 App Router인데, 13버전 이전에는 Pages Router를 사용했다고 하는데 바뀌었다고 한다.
하지만 개발 방식이 많이 다르다고 하니, 일단 나는 최신 버전으로 한번 사용해보려고 한다.

개발환경 실행

npm run dev

기본 구성

src 폴더에 들어가면 layout.js가 있고 page.js, globals.css를 볼 수 있을 것이다.
여기에서 기본 react와 다른 점은 layout.js가 next.js의 가장 큰 골격이라고 볼 수 있다.
이 안에 children이 body 태그 안에 있는데, 여기에서 children은 page.js에서 끌어오는 것이다.
그렇기 때문에 page.js 안에 있는 내용을 전부 지우고 global.css 안에 내용을 전부 지운 후 page.js에다가 Hello! 라고 넣으면 hello가 브라우저에 뜰 것이다.

배포법

package.json에 들어가면 scripts에 dev, build, start, lint가 있을 것이다.

여기에서 dev는 우리가 기존 react를 쓰면서 알고 있던 npm run start, 로컬 환경에서 브라우저로 띄우는 것.
npm run build는 배포판을 만드는 것.
npm run start는 배포판을 서비스 하는 것.

로컬로 실행을 해서 network창을 보면 리소스 용량이 6mb가 넘는 것을 볼 수 있다.
그러나 npm run build를 한 후 start로 서비스를 하면 리소스가 290kb 까지 떨어지는 것을 볼 수 있다.
즉, 로컬에서 보는 것은 실제 서비스를 시작하기에는 너무 용량이 큰 편이라는 것도 확인 가능.

Routing

기본적인 layout.js와 page.js는 약속이라고 생각하면 된다.
page.js는 layout.js에서 관리를 한다.

만약 src/app 안에 있는 layout.js에서 이런 식으로 라우팅을 한다고 생각해보자.

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <h1><a href="/">WEB</a></h1>
        <ol>
          <li><a href="/read/1">html</a></li>
          <li><a href="/read/2">CSS</a></li>
        </ol>
        {children}
        <ul>
          <li>
            <a href="/create">Create</a>
            <li><a href="/update/1">Update</a></li>
            <li><input type="button" value='delete' /></li>
          </li>
        </ul>
      </body>
    </html>
  )
}

만약 여기에서 create routing을 시키고 싶다면 src/app 밑에 create라는 폴더를 만든다.
그렇다면 이 폴더가 주소가 되는 것이다.

여기 안에 똑같이 page.js를 만들고 안에 내용을 넣으면 next.js는 page가 layout을 찾게 된다.

허나, 지금 layout을 create 안에 만들지 않았으니 layout은 부모인 src/app 안에 있는 layout에 랜더링이 된다.

위에 {children}에 create 폴더 안에 page가 뜬다는 것.

그렇다면 만약에 create 폴더에 layout.js를 만든다면?

일단 page.js는 create폴더에 있는 layout에 들어가고 그 layout이 {children}에 속하게 된다.

만약, create 안에 layout에서 page.js의 내용을 띄우려면 이런 식으로 하면 된다.

export default function Layout(props) {
    return (
        <form>
            <h2>Create</h2>
            {props.children}
        </form>
    )
}

그러면 저 props에 create 안에 page 안에 있는 내용이 들어올 것이다.

그런데 만약에 '/read/1' '/read/2' 이런 식으로 구현을 하고 싶다면?

1과 2는 그냥 예시일 뿐이고 저것을 id 등으로 받아서 바꾸려면 어떻게 해야할까?

일단 똑같이 read라는 폴더를 만든다.

그리고 read 밑에 대괄호로 싸서 id를 넣는다, [id] 이런 식으로.

그렇게 하고 그 밑에 page.js를 만들고 띄운다

export default function Read(props) {
    return (
        <>
            <h2>Read</h2>
            parameter: {props.params.id}
        </>
    )
}

이런 식으로.

이렇게 하면 일단은 params를 가지고 와서 띄울 수 있다.

LINK

next.js에서 저런 식으로 a tag로 설정을 해버리면 들어갈 때마다 다시 처음부터 전부 불러오고 기존에 들어갔던 곳을 다시 들어가도 다시 전부 다운을 다시 받는다.

이럴 때는 a tag를 LINK tag로 바꿔주면 된다.

이렇게 하였을 때, 기존에 들어갔던 곳은 아예 통신도 타지 않는 현상을 보게 될 것이다.

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <h1><Link href="/">WEB</Link></h1>
        <ol>
          <li><Link href="/read/1">html</Link></li>
          <li><Link href="/read/2">CSS</Link></li>
        </ol>
        {children}
        <ul>
          <li><Link href="/create">Create</Link></li>
          <li><Link href="/update/1">Update</Link></li>
          <li><input type="button" value='delete' /></li>
        </ul>
      </body>
    </html>
  )
}

IMG

IMG 파일을 올리는 방법은 그냥 기존에 쓰던 방식과 비슷하다

만약 public 안에 사진을 넣었다면 이런 식으로 가능하다.

export default function Home() {
  return (
    <>
      <h2>Welcome</h2>
      Hello, Next.js
      <br /><img src="/뉘에.jpeg"></img>
    </>
  )
}

여기에서 /가 public으로 되는 현상.

Server-component, client-component

우리가 react를 사용하면서 사용하는 component는 client component다.
하지만 react가 발전하면서 server component와 client component로 나뉘어 지는데, react front-end를 개발하다 보면 server component와 구분하여 사용해본 적이 없기 때문에 모를 것이다.

하지만 next.js를 사용하면 server component와 client component를 구분하여 사용할 줄 알아야 한다.

위에 보이는 것이 server component와 clinet component에서 사용할 수 있는 대표적인 것들을 보여주는 것이다.

지금 보면 보통 react front-end 개발자들이 많이 쓰는 useState, useEffect 등 훅들과 이벤트 발생하는 것들을 server component에서 사용이 불가능 한 것을 알 수 있다.

next JS는 기본적인 component는 server componenet이다.

그렇기 때문에 저런 훅들은 사용을 할 수 없는 것이다.
그렇다면 어떻게 client component로 사용을 할 수 있을까?

간단하다. component 최상단에 'use client' 를 넣어주면 client componenet로 인식하여 훅들을 사용할 수 있게 된다.

client component는 .next 폴더에 저장 후 js를 제외한 정적인 내용만 전송을 하기 때문에 용량이 적은 이점이 있다.

그래서 보통 크게 server component 는 사용자와 상호작용을 하지 않는 보여주기만 하는 기능을 할 때 사용하고 client component는 사용자와 상호작용하는, 예를 들면 button, input 등을 예로 들 수 있는데 이런 것들을 사용할 때 사용한다.

만약 페이지에 보여주는 것과 button이 있다면 button같은 것들을 따로 client component로 빼서 사용하는 것이 좋다.

글 쓰기(post)

Create라는 폴더를 만들어 page.js에서 글쓰기를 구현해 보았다.

fetch를 사용해서 구현.

'use client'

import { useRouter } from "next/navigation";

export default function Create() {
    const router = useRouter();
    return (
        <form onSubmit={(e) => {
            e.preventDefault();
            const title = e.target.title.value;
            const body = e.target.body.value;
            const options = {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ title, body })
            }
            fetch('http://localhost:9999/topics', options)
                .then((res) => res.json())
                .then(result => {
                    console.log(result);
                    const lastid = result.id;
                    router.refresh();
                    router.push(`/read/${lastid}`)
                })
        }}>
            <p>
                <input type="text" name="title" placeholder="title" />
            </p>
            <p>
                <textarea name="body" placeholder="body"></textarea>
            </p>
            <p>
                <input type="submit" value="create" />
            </p>
        </form>
    )
}

여기에서 글을 올리면 db.json에 글 목록들이 생성.

{
  "topics": [
    {
      "id": 1,
      "title": "html",
      "body": "html is ..."
    },
    {
      "id": 2,
      "title": "css",
      "body": "css is ..."
    },
    {
      "title": "ee",
      "body": "ee",
      "id": 3
    },
    {
      "title": "ee",
      "body": "ee",
      "id": 4
    },
    {
      "title": "ee",
      "body": "ee",
      "id": 5
    },
    {
      "title": "ee",
      "body": "ee",
      "id": 6
    },
    {
      "title": "ee",
      "body": "ee",
      "id": 7
    },
    {
      "title": "aaa",
      "body": "aaaaa",
      "id": 8
    },
    {
      "title": "react",
      "body": "react is ...",
      "id": 9
    }
  ],
  "posts": [
    {
      "id": 1,
      "title": "json-server",
      "author": "typicode"
    }
  ],
  "comments": [
    {
      "id": 1,
      "body": "some comment",
      "postId": 1
    }
  ],
  "profile": {
    "name": "typicode"
  }
}

하지만 여기에서 app 안에 layout.js에서 topics를 map을 하는데, title을 최신화를 못 하는 경우가 발생하였다.
cache 문제라고 한다.

router.refresh를 하고 있지만 next.js는 fetch를 하였을 때 .next directory에 저장해둬서 cache를 새로 만들거나 안 쓰는 방법으로 일단 해결은 할 수 있다.

하지만 성능이 떨어질 수 있으니, Revalidating 및 cache를 자세하게 공부하기 필수.

app 폴더 밑에 layout에서 fetch를 받던 것을 cache를 사용하지 않도록 수정한다.

  const resp = await fetch('http://localhost:9999/topics', { cache: 'no-store' });
  const topics = await resp.json();

이런 식으로 하면 cache를 사용하지 않고 글을 썼을 때 최신화가 된다.

gql을 사용하였을 때도 이런 일이 있었는데, 비슷한 경우가 생긴 것 같다.

server component에서 특정 부분 client component화 시키기

만약에 app 폴더 안에 layout.js에서 특정 부분만 client component화가 필요하다면 어떻게 해야 할까?

전체를 client component화 시킬 수 있지만, 그렇게 한다면 server comopnent의 장점을 살리지 못하기 때문에 특정 부분만 client component화를 시킬 수 있다.

앞전에도 말했지만 server component를 정보를 보여주는, 그리고 client component는 사용자와 상호작용하는 부분을 말한다.

그렇기 때문에 만약 내가 param를 가지고 움직이고 싶으나 server component에서는 params를 가져올 수 있는 useParams를 사용할 수 없기 때문에 부분적으로 clinet component화를 시켜주는 것이 좋다.

import Link from 'next/link'
import './globals.css'
import { Control } from './Control';

export const metadata = {
  title: 'Web tutorials',
  description: 'Hong Wang yeul',
}

export default async function RootLayout({ children }) {
  const resp = await fetch('http://localhost:9999/topics', { cache: 'no-store' });
  const topics = await resp.json();


  return (
    <html>
      <body>
        <h1><Link href="/">WEB</Link></h1>
        <ol>
          {topics.map((topic) => {
            return <li key={topic.id}><Link href={`/read/${topic.id}`}>{topic.title}</Link></li>
          })}
        </ol>
        {children}
        <Control />
      </body>
    </html>
  )
}


layout.js
'use client'
import Link from 'next/link';
import { useParams } from 'next/navigation';

export function Control() {
   const params = useParams();
   const id = params.id;
   return (
       <ul>
           <li><Link href="/create">Create</Link></li>
           {id ? <>
               <li><Link href={"/update/" + id}>Update</Link></li>
               <li><input type="button" value='delete' /></li>
           </> : null
           }

       </ul >
   );
}


// 부분 client component화

환경변수

기본 환경변수를 적용하는 것은 react와 조금 차이가 난다.

react는 .env에 적용하는 반면 next.js는 기본 설정이 .env.local이다.

안에 설정하는 것은 server component같은 경우는 기존과 같다.

API_URL=http://localhost:9999/

하지만 client component에서는 보안상 이런 식으로는 되지 않는다.

만약에 clinet component에서 이런 식으로 환경변수를 사용하고 싶다면

NEXT_PUBLIC_API_URL=http://localhost:9999/

이런 식으로 앞에 NEXT_PUBLIC을 붙여줘야 한다.

기억 또 기억.

이 모든 것은 생활코딩으로 기본 tutorial을 본 것을 복습할겸 기록해둔 것입니다.
가장 좋은 것은 생활코딩에서 NEXT.JS 강의를 보시는 것이 제일 좋을 것입니다.

Loading

사용법은 어렵지 않다.

loading을 띄워주고 싶은 곳, 즉 폴더에다가 loading.tsx를 만든 후 넣고싶은 대로 넣으면 된다.
주의할 점은 laout은 그대로 보여주고 Page 부분을 loading을 돌린다는 점.

Error

이것 또한 Loading과 비슷하다.

error 을 띄워주고 싶은 곳, 즉 폴더에다가 error.tsx를 만든 후 넣고싶은 대로 넣으면 된다.
차이라면 useEffect를 쓸 수도 있고 다양한 방식으로 쓰일 수 있으니 보통 client component에서 사용하고 또한 홈페이지에 있는 기본 구조를 보면 프롭스로 받아서 사용하고 있다는 걸 확인할 수 있다.

'use client' // Error components must be Client Components
 
import { useEffect } from 'react'
 
export default function Error({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  useEffect(() => {
    // Log the error to an error reporting service
    console.error(error)
  }, [error])
 
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button
        onClick={
          // Attempt to recover by trying to re-render the segment
          () => reset()
        }
      >
        Try again
      </button>
    </div>
  )
}

Image

image는 기존 리액트와 비슷하나 nextjs에서 만든 Image tag가 따로 있다.
이게 브라우저에서 최적화를 시켜주기 때문에 이것을 사용하는 것이 좋다.

만약 사진을 가지고 있을 경우에는 그 사진의 경로를 불러와서 src에 띄워주면 된다.

import picture from '경로'

...

return <Image src={picture} alt='사진' priority />

// 만약 사진이 여러개인데 중요하게 먼저 불러줘야 할 게 있다면 뒤에 priority를 붙인다

만약 url로 사진을 가져올 경우는 일단 import를 하지 않고 넣는 방식은 똑같다.
하지만 width와 height를 필수적으로 넣어주어야 하며 next.config.js에도 넣어줘야 한다.

return <Image src=url alt='사진' width={599} height={122} priority />

const nextConfig = {
    images: {
        remotePatterns: [
            { protocol: 'https', hostname: '블라블라' }
        ]
    }
}

Font

font는 next/font가 따로 있다.
기존 font 적용에는 layout shift가 발생하여 폰트마다의 특징들 때문에 다시 크기를 맞춰야 하는 경우가 있지만, next js에서 제공하는 next/font는 layout shift가 발생하지 않는다.

기본 적용하는 방법은 import를 한 후 적용하고 싶은 곳에 className에 넣어서 적용.

전체 적용하려면 당연히 app 폴더에 있는 layout에서 적용해야겠죠?

import { Nanum_Gothic } from 'next/font/google';

const gothic = Nanum_Gothic({
    weight: '700',
    subsets: ['latin'],
});

중간 생략

 <html lang='en' className={gothic.className}>
            <body>
                <header className={styles.header}>
            
                </header>
                {children}
            </body>
        </html>

Redirect

Nextjs에서 정적으로 redirect는 next.config에서 설정하면 된다.

async redirects() {
        return [
            {
                // 어떤 경로
                source: '어디 경로',
                // 어디로
                destination: 'redirect 할 경로',
                // 영구적으로 이동
                permanent: true,
            },
            {
                source: '어디 경로,
                destination: 'redirect 할 경로',
                permanent: false,
            }
        ]
    }

하지만 동적으로 사용하는 것은 얘기가 달라지는데, 그냥 React에서 썼던 것처럼 사용하면 된다.

redirect가 next/navigation으로 제공되는 것을 import 시키고 redirect('이동할 주소')를 넣으면 된다.

import { redirect } from 'next/navigation';

redirect('redirect 할 경로');

Rewrite

redirect와 방식은 같고 permanent 같은 건 없다.
Redirect와의 차이는 redirect는 경로를 변경해서 이동을 시켜주는 느낌이라면 rewrite는 보안상 key들이 들어가있다든가 복잡한 url을 대체 및 덮어씌울 수 있다는 것이다.

source 부분에 어떤 주소를, detination에 어떻게 대체할 것인가를 입력하면 destination 주소를 입력하면 내부적인 주소로 이동을 한다.

useRouter

nextjs에는 react에서 navigation으로 사용했던 것이 useRouter라는 것으로 사용할 수 있다.

import 후 해당 router에 push를 하면 되는 구조이다.

import { useRouter } from 'next/navigation';

        <button
            onClick={() => {
                router.push('이동할 경로');
            }}
        >
            제품 페이지로 이동
        </button>

middleware

미들웨어란 문지기같은 역할.

특정한 상황에서 특정한 것을 검사를 하는 것이다.

import { NextRequest, NextResponse } from 'next/server';

// 전체 페이지에서 작동함
export function middleware(request: NextRequest) {
    console.log('middleware');
    if (request.nextUrl.pathname.startsWith('/product/1004')) {
        console.log('미들웨어 - 경로 리다이렉팅');
        return NextResponse.redirect(new URL('/product', request.url));
    }
}

// 특정한 페이지에서만 가능하도록 설정(config)
export const config = {
    matcher: ['/product/:path*'],
};
profile
코딩 일기장

0개의 댓글