Learn/UPSIDE

8 - Next 기반 프로젝트

Nithen 2025. 3. 2. 20:55
728x90

Overview

이번 시간에는 우리의 Hello, World!가 어떤 식으로 화면에 출력되는지, 프로젝트 구조의 관점에서 알아봅시다. 자세한 원리보다는 요소들의 상호작용에 집중할테니, 대강 "그렇구나~" 하고 다음에 이어질 프레임워크 위에서의 작업에 대한 감각을 익혀두는 정도면 충분합니다.


Hello, World!

최종 사용자가 마주하는 결과에서부터 차례대로 거슬러 올라가봅시다. 먼저, 새롭게 개편된 우리의 랜딩 페이지입니다. 단순히 텍스트만 있던 이전 단계보다 더 발전되었죠? 하지만 "World" 문자열은 여전히 이전과 동일하게 GET /api/name 요청을 통해 전달받습니다. 이러한 내용이 어디에 어떻게 구현되었는지 살펴봅시다.  

🚀 pnpm dev ➡️ http://localhost:3000 🌐

src/app/page.tsx

가장 먼저 tsx라는 확장자가 눈에 들어옵니다. 일반적으로 우리가 사용하는 자바스크립트 확장자는 js였죠? 이제 React 라이브러리를 사용하면서 jsx 파일을 활용하게 되었습니다. JSX는 공식적으로 JavaScript XML이라 부르며, JavaScript Syntax Extension으로도 불립니다. 핵심은 이를 통해 HTML과 자바스크립트를 통합하여 관리하고, 작성한 컴포넌트의 변경을 빠르고 안정적으로 처리할 수 있게 되었다는 것입니다. 화면에 새로운 요소를 추가하고 싶다면 JSX 코드를 추가하면 됩니다. 마지막으로 TSX는 JSX에 단순히 타입스크립트 지원을 위한 확장자입니다. 일반 js 코드는 ts로, jsx 코드는 tsx로 확장자를 설정해야 타입스크립트의 강력한 타입 체크 기능을 활용할 수 있습니다. JSX에 대한 자세한 내용은 facebook.github.io/jsx를 참고하세요!

import helloWorld from "@/public/hello-world.jpeg";
import Image from "next/image";

async function Title() {
  const url = `http://localhost:3000/api/name`;
  const title = await fetch(url)
    .then((res) => res.json())
    .then((data) => data.result);

  return <div id="greeter">Hello, {title}!</div>;
}

export default async function RootPage() {
  const title = await Title();
  const banner = <Image src={helloWorld} ... />;
  return (
    <main>
      {title}
      <div>
        <div">{banner}</div>
      </div>
    </main>
  );
}

 

page.tsx에서 핵심만 추려낸 코드입니다.

  1. 가장 먼저 함수 Title()"Hello, World!"를 출력하도록 구현되었습니다. 이전 브랜치에서 구현했던 것처럼 GET /api/name 요청의 결과를 참조합니다. 한 가지 눈 여겨볼 점은 단순히 문자열을 반환하지 않고, JSX 태그를 반환하는 것을 알 수 있습니다. 그리고 태그 안에 중괄호 { }를 통해 JSX 표현식을 사용하여 값을 집어넣고 있네요.
  2. Next.js 프레임워크에서는 app 폴더 내에 존재하는 page.tsx 파일에서 export default 키워드로 노출된 함수를 화면에 출력합니다. 따라서, RootPage()라는 함수의 내용이 우리의 화면에 나오게 되는 것이죠.

위 내용을 이해했다면 GET / 이전에 GET /api/name 요청이 왜 먼저 처리되는지 알 수 있습니다. 

GET http://localhost:3000

src/app/api/name/route.ts

위에 찍힌 Anvil 로그를 보니, on-chain 데이터를 가져오는 것 같은데, 이 내용은 어디에서 처리되는 걸까요? Next.js에서는 컨벤션 기반 내부 라우팅을 지원합니다. 그래서 page.tsx 대신 route.ts를 작성하는 것으로 custom request handler를 만들 수 있습니다. getName 함수를 받아 실행하는 간단한 구조네요! 특별히 이해하기 어려운 부분은 없을거라 생각해서 넘어가겠습니다. 혹시라도 자세한 내용이 궁금하다면 공식 문서를 참고하세요!

import { getName } from "./getName";

export async function GET() {
  const name = await getName();
  return Response.json({ result: name });
}

src/module/Sum.ts

export default function sum(a: number, b: number): number {
  return a + b;
}

 

두 숫자를 더하는 파일 sum.ts의 함수 sum입니다. 그리고 이를 테스트하는 코드가 담긴 파일 Sum.test.ts는 다음과 같습니다. suite와 describe, 그리고 test와 it는 alias 관계이기에 둘은 정확히 같은 테스트입니다. 이전 포스트에서 문자열 "1"과 숫자 2를 더하는 바람에 12가 반환된 사례가 있었는데, 이런 테스트가 존재했다면 문제를 빠르게 식별할 수 있었겠죠? 추가로 좋은 테스트를 작성하는 방법은 많은 경험을 요구합니다. 각 단위 테스트는 modularity를 보장하기 위해 mocking 등의 수단을 활용하는데, 이에 관해서는 차후 다른 포스트에서 자세히 다루도록 하겠습니다.

import assert from "node:assert";
import { describe, it, suite, test, todo } from "node:test";
import sum from "./Sum";

suite("Sum:suite", () => {
  test("should add two numbers", () => {
    assert.strictEqual(sum(1, 2), 3);
  });
});

describe("Sum:describe", () => {
  it("should add two numbers", () => {
    assert.strictEqual(sum(1, 2), 3);
  });
});

__tests__/e2e/index.spec.ts

playwright 라이브러리를 활용한 엔드-투-엔드 테스트 코드입니다. css selector를 활용하여 우리가 원하는 태그를 지정하고, 내부의 문자열이 Hello, World!를 반환하도록 검사합니다. 

import { expect, test } from "@playwright/test";

test.beforeEach(async ({ page }) => {
  await page.goto("/");
});

test("test", async ({ page }) => {
  const greeter = page.locator("#greeter");
  await expect(greeter).toHaveText("Hello, World!");
});

마치며

이번 시간에는 기존 작업물이 프레임워크에 어떻게 적용되었는지, Next.js 기반의 웹 애플리케이션의 구성요소와 두 종류의 테스트에 대한 기본적인 설명을 진행했습니다. 지금까지 잘 따라왔다면, 웹 애플케이션을 개발하기 위한 기본적인 요소는 모두 학습했다고 볼 수 있습니다. 정확하게 이해되지 않았더라도 괜찮습니다. 앞으로 구체적인 기능을 구현하면서 계속 반복해서 학습하다보면 자연스럽게 숙달될 것이라 생각합니다.

 

다음 시간부터는 본격적으로 DApp으로서의 전환에 필요한 작업들을 수행하게 됩니다. 먼저, 로컬 Anvil 서버에서 작업하는 환경을 외부 Sepolia Ethereum Testnet으로 전환할 것이고, 이 과정 중에 viem 라이브러리를 활용해서 보다 추상화된 형태로 로직을 처리할 것입니다. 동시에 GitHub Pages를 통해 외부에서 접근 가능한 형태로 애플리케이션을 배포할 것입니다. 이로서 우리의 DApp은 별도의 서버 없이 탈중앙화된 방식으로 상태 변화를 유지할 수 있게 됩니다.

728x90