TIL 90 - Learn Next.js (4chapter ~ 8chapter)

김영현·2024년 5월 14일
0

TIL

목록 보기
102/129

라우팅, 레이아웃, 페이지

react-router를 이용하여 라우팅을 했을땐, 라우팅 관련 코드를 작성해야했다.
Next에서는 File-system routing을 사용해서 폴더가 URL segement에 매핑되게 만든다.

app/dashboard/invocies라는 폴더를 생성하면, 위와같은 라우팅이 자동으로 연결된다.
또한, react-router에서 children을 이용하여 레이아웃을 작성하던 것 처럼 layout.tsx라는 파일을 각 라우트 폴더에 만들면, 자동으로 적용해준다.

경로로 접속했을때 받아오는 파일은 각 폴더 내 page.tsx다. 즉, 고정된 네이밍으로 파일을 만들어야 한다.

라우팅

// app/dashboard/page.tsx
export default function Page() {
  return <p>Dashboard Page</p>;
}

간단하구먼?

레이아웃

폴더 내부에 layout.tsx를 생성하면, 라우팅 기준으로 레이아웃을 적용해준다.
현재는 app/dashboard/layout.tsx를 생성하였다.

//app/dashboard/layout.tsx
import SideNav from '@/app/ui/dashboard/sidenav';

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex h-screen flex-col md:flex-row md:overflow-hidden">
      <div className="w-full flex-none md:w-64">
        <SideNav />
      </div>
      //layout이니까 당연히 children을 렌더링 해주어야 한다. 이때 children은 dashboard를 포함한 하위 라우트다.
      <div className="flex-grow p-6 md:overflow-y-auto md:p-12">{children}</div>
    </div>
  );
}

영향을 받는 라우트는 dashboard와 그 하위 /customers, /invoices다.



원래는 존재하지 않았던 Navigationbar가 layout으로 인하여 생겼다!

최상단에서 적용하는 RootLayout도 있다. 이는 app폴더 하위에 생성하면 된다.

현재 적용중인 루트레이아웃은 아래와 같다.

//app/layout.tsx
import '@/app/ui/global.css';
import { inter } from './ui/fonts';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={`${inter.className} antialiased`}>{children}</body>
    </html>
  );
}

페이지 간 탐색

react-router와 유사하게 Link컴포넌트를 사용하여 탐색한다. 그리고 경로별 코드 스플리팅을 자동으로 지원한다.
즉, 자동으로 최적화를 하고 특정페이지에서 오류가 발생하더라도 앱의 나머지부분은 정상작동 한다는 뜻이다.

또한 prod환경에서 <Link/>컴포넌트가 뷰포트에 나타날 때 마다 연결된 경로에 대한 코드를 pre-fetching한다.

마지막으로 라우터 캐시라는 걸 사용한다. 즉, Next.js는 방문한 경로 세그먼트를 캐싱한다!
이 캐시는 세션동안 저장한다. => 탭을 닫거나 새로고침 하면 없어진다.
아니면 일정 시간 이후 지워진다.

여기서 prefetching방식이 두가지로 나누어져 있는 걸 볼 수 있다.

  • default prefetching : 정적 경로면 전체, 동적 경로면 가장 가까운 loading.js까지 부분경로를 prefetching한다.
  • full prefetching : 전부

아직 정확히는 모르겠지만, loading.js라는 네이밍을 이용하면 Suspense같은 선언적 컴포넌트와 비슷한 기능을 하는게 아닐까 추측해본다.

usePathname()

라우팅을 지원하는 프레임워크답게 현재경로를 가져오는 hooks도 내부적으로 지원해준다.
다만 을 사용하기위해서는 client component로 바꿔주어야할 필요가 있다고 쓰여있다.
그러니까 파일 최상단에 'use client'라는 문자열을 입력해주어야한다. 마치 'use strict'같은데 뭔지는 아직 모르겠다.

과연 서버 포넌트, 클라이언트 컴포넌트는 무얼 말하는걸까?
일단 서버에서 렌더링되어 넘어오는 컴포넌트, 클라이언트에서 렌더링되는 컴포넌트라고 가볍게 이해하고 넘어가보자.
새로운 기술을 배울땐 하향식 접근법(top-down)이 최고다!


DB설정

프론트에서 DB를 다룰수 있다니! 일단 공식문서에서는 vercel로 배포하라고 한다.
레포지토리 만들어서 배포한 뒤, storage설정을 해준다.

그다음 DB에 접근할수 있는 키들을 복사해온다.

Next측에서 준비해준 .env파일에 복사해온 값들을 넣어주고 npm i @vercel/postgres를 이용하여 vercel에서 postgres를 사용할수 있게 해주는 패키지를 다운!

이후 Next에서 제공해준 스크립트를 실행해주면 된다. 아래는 코드 일부를 가져와봤다.

const { db } = require('@vercel/postgres');

async function seedUsers(client) {
  try {
    await client.sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`;
    // Create the "users" table if it doesn't exist
    const createTable = await client.sql`
      CREATE TABLE IF NOT EXISTS users (
        id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
        name VARCHAR(255) NOT NULL,
        email TEXT NOT NULL UNIQUE,
        password TEXT NOT NULL
      );
    `;

    console.log(`Created "users" table`);

    // Insert data into the "users" table
    const insertedUsers = await Promise.all(
      users.map(async (user) => {
        const hashedPassword = await bcrypt.hash(user.password, 10);
        return client.sql`
        INSERT INTO users (id, name, email, password)
        VALUES (${user.id}, ${user.name}, ${user.email}, ${hashedPassword})
        ON CONFLICT (id) DO NOTHING;
      `;
      }),
    );

    console.log(`Seeded ${insertedUsers.length} users`);

    return {
      createTable,
      users: insertedUsers,
    };
  } catch (error) {
    console.error('Error seeding users:', error);
    throw error;
  }
}
...

async function main() {
  const client = await db.connect();

  await seedUsers(client);
  await seedCustomers(client);
  await seedInvoices(client);
  await seedRevenue(client);

  await client.end();
}

main().catch((err) => {
  console.error(
    'An error occurred while attempting to seed the database:',
    err,
  );
});

쿼리문을 사용하여 유저 테이블을 생성한다. 이후 Promise.all을 이용하여 병렬(처럼보이는)처리를 해주어 유저의 계정을 생성한다.
마지막으로 main함수에서 db연결 후 모든 쿼리를 처리한 뒤 db연결을 해제한다.

DB쪽은 문외한이지만, 코드를 보니 흐름을 알 수 있다. 쉽게 읽히는 코드인 만큼 잘 작성된 코드다. 역쉬 Next...
아무튼 이렇게 쿼리를 생성하면, Vercel의 Storage-Data측에 실제로 데이터가 입력된다.


Data Fetching

DB에서 데이터를 가져오는 방법은 보통 API레이어를 이용한다.
DB관련 로직이 존재하는 Api서버에 api-client(fetch,axios등)으로 요청을 보내면, Api서버에서 쿼리를 해주어 데이터를 가져온다.

왜 클라이언트에서 DB를 직접 조작하지 않을까? => DB가 노출되면 굉장히 위험하므로 자제한다.

아무튼 이런 방식이 보통인데, 놀랍게도 Next에서는 Api서버를 제작할 수 있다. 프론트에서 어떻게...?😮
일단 만들 수 있다는 사실만 알아두고 넘어가보자

서버컴포넌트를 이용하여 데이터를 가져온다

Next.js는 기본적으로 React Server Components를 이용하여 데이터를 가져온단다. 장점은 아래와 같다.

  • Promise를 지원하고 useEffect,useState를 사용하지 않고도 async/await구문을 지원한다.
  • 서버에서 실행되므로 비용이 많이드는 data-fetching로직을 서버에만 보관하고 결과만 클라이언트로 보낼 수 있다.
  • 위 이유덕분에 별도의 Api layer를 작성하지 않고도 DB에 직접 쿼리할 수 있다.

오우? 프론트에서 DB를 쿼리하는데, 실제로 프론트에는 코드가 노출되지 않는다.
이렇게되면 프론트에서 DB를 다룰 수 있으니, 풀스택인가? 다만 DB서버는 알아서...

실제로 서버 컴포넌트에 접근하면, 네트워크 fetch/xhr탭에서 컴포넌트를 가져오는 걸 볼 수있다.

async component

신기한건 컴포넌트를 async함수로 만들 수 있다는 점이다. 이를 통해 데이터를 가져올 수 있다.

export default async function Page() {
  const revenue = await fetchRevenue();
  const latestInvoices = await fetchLatestInvoices();
  const {
    numberOfCustomers,
    numberOfInvoices,
    totalPaidInvoices,
    totalPendingInvoices,
  } = await fetchCardData();
  
return....
}

서버컴포넌트만 가능하니 유의하자

request waterfall(Promise.all)

async/await문법을 사용하면, async내부 함수는 blocking되고 바깥 환경의 함수가 계속 실행된다.
바로 이전 예시를 봐보면, 세 개의 await 함수가 연속적으로 실행된다. 따라서 요청이 병렬적으로 실행되는 것이 아닌, 연속적으로 실행된다.

즉, 제일 오래걸린 요청 시간만큼 걸리는게 아닌, 모든 요청시간을 더해야 비로소 데이터가 정상적으로 도착한다는 것이다.
이 패턴이 나쁘다고 할 수는 없다. 이전 데이터가 꼭 필요한 경우 await으로 blocking을 걸어두고 데이터가 도착한 다음 실행하는 게 좋다. 하지만 각각의 데이터가 독립적일때, 굳이 await으로 막아둘 필요가 없다.

이는 MyPromise 만들기에서 만들며 깨우친 Promise.all을 이용하면 되지 않을까?

실제로 내부 Next의 예시 내부 코드는 이렇게 생겼다.

//card 정보를 가져오는 쿼리 내부

    const data = await Promise.all([
      invoiceCountPromise,
      customerCountPromise,
      invoiceStatusPromise,
    ]);

여러 데이터를 Promise.all을 이용해 처리한다.

이 역시 단점이 존재한다. 하나의 데이터 요청이 다른 요청보다 훨씬 오래걸리는 경우!
기술에 정답은 없다. 언제나 장단점을 잘 숙지하고 필요할때 사용하자!


정적(static), 동적(dynamic) 렌더링

Next는 정적, 동적 렌더링을 둘 다 지원한다.

  • 정적 렌더링 : 빌드(배포)시 데이터 가져오기 및 렌더링을 서버에서 처리한다. 결과는 CDN에 캐싱될 수 있다. 보통 사용자 간 공유되는 데이터가 없거나 데이터가 없는 UI에 유용하다! ex)개인 정적 블로그
  • 동적 렌더링 : 요청시 콘텐츠가 서버에서 렌더링된다.

unstable_noStore

페이지 캐싱을 비활성화하여 동적 렌더링페이지로 바꿀 수 있다. 즉, 사용자가 항상 최신 정보를 보게 하는 것이다.

import { unstable_noStore as noStore } from 'next/cache';

noStore(); //fetch함수 내부에서 사용하면 된다.

하나의 데이터 요청이 다른 요청보다 오래걸리는 경우

try {
    console.log('Fetching revenue data...');
    await new Promise((resolve) => setTimeout(resolve, 3000));

    const data = await sql<Revenue>`SELECT * FROM revenue`;

    console.log('Data fetch completed after 3 seconds.');

    return data.rows;
  } catch (error) {
    console.error('Database Error:', error);
    throw new Error('Failed to fetch revenue data.');
  }
}

위처럼 강제로 3초를 지연시키면, 페이지자체가 3초뒤에 렌더링된다. 즉, noStore를 사용하여 동적 렌더링을 만들었지만, 데이터를 가져오는 속도가 느려 사용자가 페이지에 재접속 할 경우 계속 3초간 지연된다는 소리다.

이를 해결할 수 있는 방법이 뭐가 있을까?


Streaming

이부분부터 내일 진행해보도록 하겠습니다!

profile
모르는 것을 모른다고 하기

0개의 댓글