Next.js


자바스크립트 웹 프레임워크

개요

  • Next.js는 자바스크립트 웹 프레임워크이다.
  • 리액트 문법을 사용할 수 있다.
  • 리액트에는 없는 서버 사이드 렌더링(SSR) 기능을 제공한다.
  • 프로젝트 시작

  • 프로젝트 생성
  • npx create-next-app@latest
  • 개발 환경으로 실행
  • npm run dev
  • 프로젝트 파일들
  • 폴더 방식 라우팅

  • next.js에서는 폴더 이름을 통해 라우팅 주소를 설정해줄 수 있다.
  • 이렇게 하면 http://localhost:3000 에서는 app폴더 바로 아래의 page.js가 보이게 된다.
  • http://localhost:3000/list 에서는 list폴더 바로 아래의 page.js가 보이게 된다.
  • layout.js는 해당 폴더에 있는 page.js를 감싼다.
  • 따라서 navigation bar와 같은 것을 만들고 싶다면 가장 상위의 layout.js에 이를 넣어줄 수 있다.
  • next.js에서도 리액트와 같이 Link 태그를 제공한다.
  • // /app/layout.js
    export default function RootLayout({ children }) {
    return (
    <html lang='en'>
    <body>
    <div className='navbar'>
    <Link href='/'>home</Link>
    <Link href='/list'>list페이지</Link>
    </div>
    {children}
    </body>
    </html>
    );
    }
  • 여기서 받은 children은 /app/page.js 이다.
  • 이미지 최적화

  • React에서 처럼 img 태그를 이용하여 public 폴더(/)에 있는 이미지를 삽입할 수도 있지만 Next.js에서는 <Image /> 라는 태그를 제공한다.
  • 이 태그를 사용하면 자동으로 이미지 lazy loading & 사이즈 최적화 & layout shift 방지를 해주어 성능과 속도를 개선할 수 있다.
  • import Image from 'next/image'
    import 이미지 from './food0.png'
    export default function Home() {
    return(
    <div>
    <Image src={이미지} alt="옥수수"/>
    <div/>
    )}

    Server/Client Component

  • Next.js에서는 기본적으로 만드는 컴포넌트들이 Server Component로 작동한다.
  • 그러나 Server Component에서는 아래와 같은 기능을 사용할 수 없다.
  • 이러한 기능을 사용하려면 Client Component로 만들어야 하며, 파일 최상단에 'use client' 라고 적어주면 된다.
  • 'use client';
    import { useState } from 'react';
    export default function ListItem({ item, idx }) {
    const [count, setCount] = useState(0);
    const onPlusClick = () => {
    setCount(count + 1);
    };
    return (
    <div>
    <img src={`/images/food${idx}.png`} alt='음식' />
    <h3>{item}</h3>
    <span>{count}</span>
    <button onClick={onPlusClick}>+</button>
    </div>
    );
    }
  • 따라서 SSR의 이점을 살리기 위해 상위 페이지는 Server Component로, 해당 페이지에서 위 기능이 필요한 부분만 Client Component로 만드는 것이 효율적이다.
  • ‘use client’를 사용하여 Client Component로 만들면 그것으로부터 import된 모든 모듈이 Client Component로 간주된다.
  • 서버 컴포넌트와 클라이언트 컴포넌트의 사용 사례

    패턴

    MongoDB 연결하기

  • 서버 컴포넌트는 바로 MongoDB에 연결하여 DB의 데이터를 꺼내올 수 있다.
  • // /util/database.js
    import { MongoClient } from 'mongodb';
    const url = process.env.NEXT_PUBLIC_MONGO_CODE;
    const options = { useNewUrlParser: true };
    let connectDB;
    if (process.env.NODE_ENV === 'development') {
    if (!global._mongo) {
    global._mongo = new MongoClient(url, options).connect();
    }
    connectDB = global._mongo;
    } else {
    connectDB = new MongoClient(url, options).connect();
    }
    export { connectDB };
    // /app/list/page.js
    import { connectDB } from '@/util/database';
    import Link from 'next/link';
    export default async function List() {
    const client = await connectDB;
    const db = client.db('forum');
    const posts = await db.collection('post').find().toArray();
    return (
    <div className='list-bg'>
    {posts.map(el => (
    <div key={el._id} className='list-item'>
    <h4>{el.title}</h4>
    <p>{el.content}</p>
    </div>
    ))}
    </div>
    );
    }

    Dynamic Route

  • 폴더 라우팅 방식에서 Dynamic Route를 하려면 다음과 같이 할 수 있다.
  • 우선 폴더명에 대괄호를 씌우고 임의의 이름을 만들어준다.
  • 현재는 /detail/[id]/page.js 의 형태로 만들어져 있다.
  • 해당 page.js에 props를 받아 출력하면 다음과 같이 나온다.
  • // /app/detail/[id]/page.js
    export default async function Detail(props) {
    console.log(props);
    return <div></div>;
    }
    // http://localhost:3000/detail/647ef278724cb7262751b0ed 로 접속했을 때의 콘솔
    { params: { id: '647ef278724cb7262751b0ed' }, searchParams: {} }
  • 폴더명으로 정한 [] 안의 이름이 props.params 의 키값이 되어 사용자가 접속한 url을 전달해주고 있다.
  • 이를 이용하여 다음과 같이 코드를 작성할 수 있다.
  • // /app/detail/[id]/page.js
    import { ObjectId } from 'mongodb';
    import { connectDB } from '@/util/database.js';
    export default async function Detail({ params }) {
    const db = (await connectDB).db('forum');
    const result = await db.collection('post').findOne({ _id: new ObjectId(params.id) });
    return (
    <div>
    <h1>상세페이지</h1>
    <h4>{result.title}</h4>
    <p>{result.content}</p>
    </div>
    );
    }
  • List 컴포넌트에서는 Link 태그를 이용하여 해당 ObjectId를 전달하도록 한다.
  • // /app/list/page.js
    import { connectDB } from '@/util/database';
    import Link from 'next/link';
    export default async function List() {
    const client = await connectDB;
    const db = client.db('forum');
    const posts = await db.collection('post').find().toArray();
    return (
    <div className='list-bg'>
    {posts.map(el => (
    <Link key={el._id} href={`/detail/${el._id}`}>
    <div className='list-item'>
    <h4>{el.title}</h4>
    <p>{el.content}</p>
    </div>
    </Link>
    ))}
    </div>
    );
    }

    useRouter

  • Link 태그로 페이지를 이동시킬 수도 있지만 useRouter를 이용할 수도 있다.
  • 이는 클라이언트 컴포넌트에서만 사용이 가능하다.
  • 'use client';
    import { useRouter } from 'next/navigation';
    export default function DetailLink({ id }) {
    const router = useRouter();
    return (
    <button
    onClick={() => {
    router.push(`/detail/${id}`);
    }}>
    상세보기
    </button>
    );
    }
  • 이렇게 사용하면 /detail/${id} 로 이동한다.
  • 외에도 다른 메소드들이 있다.
  • 이 밖에도 next/navigation 에서 꺼내 쓸 수 있는 메소드들은 다음과 같은 것들이 있다.
  • 클라이언트 컴포넌트에서 DB데이터 꺼내기

  • client component에서 DB데이터를 꺼낼 때 직접 서버에 요청을 보내 데이터를 가져오면 SEO 측면에서 좋지 않다.
  • 이는 자바스크립트 코드가 html이 로드된 이후에 실행되어서 검색엔진 봇이 해당 데이터를 늦게 수집하기 때문이다.
  • 따라서 client component에서 DB데이터를 가져오려면 상위 server component에서 DB데이터를 가져온 후 props로 전송하는 것이 좋다.
  • 'use client';
    import Link from 'next/link';
    import DetailLink from './DetailLink';
    import axios from 'axios';
    export default function Listitem({ posts }) {
    const removePost = async (id) => {
    const res = await axios.delete(`http://localhost:3000/api/remove/${id}`);
    console.log(res.data);
    };
    return (
    <>
    {posts.map((el) => (
    <div key={el._id} className='list-item'>
    <Link href={`/detail/${el._id}`}>
    <h4>{el.title}</h4>
    </Link>
    <p>{el.content}</p>
    <DetailLink id={el._id} />
    <button onClick={() => removePost(el._id)}>삭제</button>
    </div>
    ))}
    </>
    );
    }
  • 여기서는 onClick 기능을 사용하기 위해 client component를 사용할 수 밖에 없다.
  • 서버 컴포넌트에서 req.query 사용하기

  • 위 코드르 보면 delete 메소드에 params에 id를 담아 보내는 것을 알 수 있다.
  • 저 api 경로를 서버에서 req.query로 받으려면 다음과 같은 폴더 구조를 가져가면 된다.
  • import { connectDB } from '@/util/database';
    import { ObjectId } from 'mongodb';
    export default async function write(req, res) {
    if (req.method === 'DELETE') {
    try {
    const client = await connectDB;
    const db = client.db('forum').collection('post');
    const result = await db.deleteOne({ _id: new ObjectId(req.query.id) });
    return res.status(200).send('게시글 삭제 성공');
    } catch (err) {
    console.error(err);
    }
    }
    }
  • 안에서는 이렇게 처리하여 게시글 삭제 기능을 구현했다.
  • Pre-rendering (Next 13 이전)

  • Next.js는 기본적으로 모든 페이지를 사전 렌더링(Pre-rendering)한다.
  • 따라서 미리 HTML 파일을 만들어두어 더 좋은 퍼포먼스를 제공하고, 검색엔진최적화(SEO)에 이점이 있다.
  • next.js는 두 가지 사전 렌더링 방법을 제공한다.
  • 이 두 가지는 언제 HTML 파일을 생성하는지에 차이점이 있다.
  • Static Site Generation(SSG)

  • 정적 생성 방법으로, 프로젝트가 빌드하는 시점에 HTML 파일들이 생성된다.
  • 미리 빌드된 HTML을 사용자의 요청이 들어올 때마다 재사용한다.
  • 정적 생성된 페이지들은 CDN에 캐시가 되고, 퍼포먼스를 이유로 Next.js는 정적 생성을 권고한다.
  • 유저가 요청하기 전에 미리 페이지를 만들어놔도 된다면 SSG을 사용하면 된다.
  • 그러나 데이터가 계속 바뀌어야하는 페이지라면 SSR을 사용하면 된다.
  • getStaticProps / getStaticPaths
  • Server Side Rendering(SSR)

  • HTML 파일이 매 요청마다 생성된다.
  • 따라서 항상 최신 상태를 유지한다.
  • getServerSideProps
  • Incremental Static Regeneration(ISR)

  • 일정 주기마다 데이터의 최신 여부를 검사해서 업데이트된 데이터로 다시 HTML을 생성한다.
  • 콘텐츠가 업데이트 되었을 때 이것이 반영되지 않는 SSG의 단점을 보완한 방식이다.
  • getStaticPropsrevalidate 속성을 추가하여 사용
  • App Router에서의 렌더링 (13버전)

  • Next.js의 SSR, SSG, ISR은 Fetch에 어떤 옵션을 주느냐에 따라 다르게 동작한다.
  • 그러나 각 layout.tsx에서 dynamic 옵션을 설정하면 이 옵션을 완전히 동적이거나 완전히 정적으로 바꿀 수 있다.
  • File Conventions: Route Segment Config
  • 현재 렌더링이 어떤 방식으로 되고 있는지 궁금하다면 빌드 시에 확인할 수 있다.
  • 사람 인 같은 모양이 SSR, 동그라미 모양이 SSG로 되어있다는 뜻이다.
  • App Router vs Pages Router

    Pages Router

    클라이언트 중심 라우팅

  • pages 디렉토리 사용
  • src/pages
    ├─_app.js // root layout
    ├─index.js // root page
    ├─a-page.js
    └─b-page
    └─index.js
    └─component.js // 라우팅과 관련없는 코드지만 b-page/component라는 경로가 생김
    └─b-subpage.js