[번역] Remix Tutorial - "Developer Blog" -1

Perfume·2022년 10월 5일
4

Translation

목록 보기
1/2
post-thumbnail

✍🏻

Remix 공식문서의 블로그 만들기 튜토리얼을 번역한 글입니다. 오역이나 오류가 있을 수 있음을 참고해주세요. (이해를 돕기 위해 의역이나 별도의 설명을 추가한 부분들도 있습니다.) 그렇기 때문에 가능하다면 원문을 읽고 따라하는 게 가장 좋은 방법이라고 생각합니다.

또 velog의 코드 하이라이트 기능이 아쉬운 탓에, 어떤 코드가 새로 추가된 코드인지 색깔로 따로 표기하지 못했습니다. 감안하고 읽어주세요 😸 그럼 시작합니다.

Quick Start

이 튜토리얼에서는 코드를 빠르고 간략하게 설명하겠습니다. Remix가 뭔지 15분만에 이해하고 싶다면 잘 찾아오셨습니다.

Kent의 설명과 함께 튜토리얼을 진행해보고 싶다면, Egghead.io에서 무료로 제공하는 이 강의를 참고해주세요.

본 튜토리얼은 TypeScript를 사용했습니다. 물론 Remix는 TypeScript 없이도 사용할 수 있습니다. 저희 팀은 타입스크립트를 쓸 때 가장 효율적이라고 생각하긴 하지만, 만약 타입스크립트 구문을 원치 않으시면 그냥 자바스크립트로 코드를 작성하셔도 괜찮습니다.

💿 안녕하세요, 저는 Remix의 CD, Derrick입니다 👋 이 튜토리얼을 진행하는 동안 저를 계속 보시게 될 거에요.

사전 준비

아래 링크를 클릭해서 Gitpod 작업공간을 생성해보세요. (gitpod은 간단히 말해서 브라우저를 기반으로 한 웹 개발환경(IDE)입니다.)

🔎 Gitpod: Ready-To-Code

이 튜토리얼을 Gitpod이 아니라 본인의 컴퓨터에서 로컬로 진행하시려면, 아래 항목들이 설치되어 있는지 꼭 확인해주세요.

  • Node.js version (^14.17.0, or >=16.0.0)
  • npm 7 혹은 그 이상
  • 코드 에디터 (VS code를 추천합니다.)

프로젝트 생성

⚠️ 주의! Node 버전이 14 이상인지 꼭 확인해주세요.

💿 새로운 Remix 프로젝트를 생성하겠습니다. 예시에서는 프로젝트 이름을 "blog-tutorial"로 했지만, 원하시는 대로 정하셔도 괜찮습니다.

npx create-remix --template remix-run/indie-stack blog-tutorial
? Do you want me to run `npm install`? Yes

Remix stack에 관해 더 자세히 알고 싶다면 이 문서를 확인해주세요. (참고: Remix Stacks는 Remix 프로젝트를 빠르고 쉽게 생성할 수 있는 Remix CLI 기능입니다.)

우리는 fly.io에 배포할 준비가 된 완전한 애플리케이션인 the Indie stack을 사용하고 있습니다. 여기에는 개발 도구뿐만 아니라 프로덕션 준비 인증 및 영속성이 포함됩니다. 사용된 툴에 익숙하지 않더라도 걱정하지 마세요. 진행하면서 차근차근 안내해 드리겠습니다.

참고: --template 플래그 없이 npx create-remix를 실행하면 아주 기본적인 설정값만으로 시작할 수 있습니다. 이 방식으로 생성된 프로젝트는 훨씬 간소합니다. 단, 이 경우에는 이 튜토리얼의 일부분을 진행할 때 배포할 항목을 수동으로 구성해주셔야 합니다.

💿 자, 이제 원하시는 코드 에디터에서 생성된 프로젝트를 열고 README.md 파일의 가이드를 자유롭게 읽어 보세요. 배포에 관한 내용은 나중에 뒷부분에서 살펴보겠습니다.

💿 그럼 개발 서버를 시작해봅시다.

npm run dev

💿 http://localhost:3000을 열어보세요. 앱이 동작하고 있을 거에요.

괜찮다면 잠시 시간을 내서 UI를 조금 둘러보세요. 자유롭게 계정을 생성하고 삭제해보면서 UI에서 어떤 항목들을 사용할 수 있는지 아이디어를 얻어보시는 것도 좋습니다.

첫번째 Route

자, "/posts"라는 url에서 렌더링할 새 route를 만들어보겠습니다. 먼저 link를 연결해봅시다.

💿 app/routes/index.tsx에 있는 posts로 연결 추가하기

다음 코드를 붙여넣기 해보세요.

<div className="mx-auto mt-16 max-w-7xl text-center">
  <Link
    to="/posts"
    className="text-xl text-blue-600 underline"
  >
    Blog Posts
  </Link>
</div>

원하는 위치에 붙여 넣으시면 됩니다. 저는 기술스택 아이콘들 바로 위에 작성했습니다.

혹사 위의 코드에서 tailwind의 class를 사용했다는 걸 알아차리셨나요?

Remix Indie stack은 tailwind를 지원하도록 설정되어 있습니다. 만약 tailwind를 원치 않으신다면 얼마든지 제거하시고 다른 것을 이용하셔도 좋습니다. Styling guide를 통해 Remix에서 사용 가능한 스타일링 옵션들을 확인해보세요.

자, 다시 브라우저로 돌아가서 링크를 클릭해봅시다. 아직 이 경로를 만들지 않았으니 404페이지가 보일 겁니다. 이제 route를 생성해 보겠습니다.

💿 app/routes/posts/index.tsx에 새로운 파일 만들기

mkdir app/routes/posts
touch app/routes/posts/index.tsx

mkdir나 touch 명령어를 사용하면 어떤 파일을 만들어야 하는지 명확하게 알 수 있습니다.

그냥 posts.tsx라고 이름 붙일 수도 있었지만, 곧 다른 route가 생길 것이고 그것들을 서로 붙이면 좋을 것 같아 posts/index.tsx 파일 형태로 만들었습니다. 인덱스 경로는 폴더의 경로에서 렌더링됩니다(웹 서버의 index.html처럼요).

/posts 경로로 이동하면 요청을 처리할 방법이 없다는 오류가 표시될 겁니다. 아직 그 경로에 아무것도 만들지 않았기 때문입니다! 컴포넌트를 추가한 다음 그 컴포넌트를 디폴트로 export 해봅시다.

💿 posts 컴포넌트 만들기

app/routes/posts/index.tsx

export default function Posts() {
  return (
    <main>
      <h1>Posts</h1>
    </main>
  );
}

posts 경로의 기본 페이지 레이아웃이 어떤지 보려면 브라우저를 새로 고침해야 할 수도 있습니다.

데이터 로딩하기

데이터를 로드하는 기능은 Remix에 내장되어 있습니다.

최근 몇 년 간 웹 개발을 하셨다면 데이터를 "제공"하는 API 경로와 데이터를 "사용"하는 프론트엔드 컴포넌트를 만드는데 익숙하실 겁니다. 그런데 Remix에서 프론트엔드 컴포넌트는 그 자체로 API 경로이며, 브라우저에서 서버와 자체적으로 통신하는 방법을 이미 알고 있습니다. 즉, 데이터를 가져올(fetch) 필요가 없습니다.

만약 최근 몇 년이 아니라 Rails 같은 MVC 웹 프레임워크를 쓰던 오래된(?) 시절에 개발을 하셨었다면, Remix의 routes를 리액트 템플릿을 쓴 백엔드 뷰 정도로 생각하실 수 있습니다.하지만 Remix routes를 사용하면, UI를 꾸미기 위한 별도의 JQuery를 작성하지 않고도 손쉽게 브라우저에 뛰어난 기능을 추가할 수 있습니다. 기술의 발전이 정말 눈부시지 않나요? 게다가 Remix의 routes는 그 자체로 컨트롤러이기도 합니다.

자, 그럼 컴포넌트에 일부 데이터를 제공해보겠습니다.

💿 posts 경로에 "loader"를 만드세요.

app/routes/posts/index.tsx

import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";

export const loader = async () => {
  return json({
    posts: [
      {
        slug: "my-first-post",
        title: "My First Post",
      },
      {
        slug: "90s-mixtape",
        title: "A Mixtape I Made Just For You",
      },
    ],
  });
};

export default function Posts() {
  const { posts } = useLoaderData();
  console.log(posts);
  return (
    <main>
      <h1>Posts</h1>
    </main>
  );
}

Loader는 해당 구성 요소의 백엔드 "API"이며 이미 useLoaderData를 통해 연결되어 있습니다. Remix 라우트에서 클라이언트와 서버는 경계가 뚜렷하지 않습니다. 서버와 브라우저 콘솔이 모두 열어두시면 post data에 대한 로그가 두 곳에 다 찍힌 것을 확인하실 수 있을 거에요. Remix가 서버에서 렌더링되어 기존 웹 프레임워크와 같은 전체 HTML 문서를 전송하지만 클라이언트에서도 hydration되고 거기에도 기록되기 때문입니다.

💿 게시물에 대한 link 렌더링하기

app/routes/posts/index.tsx

import { json } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";

// ...
export default function Posts() {
  const { posts } = useLoaderData();
  return (
    <main>
      <h1>Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.slug}>
            <Link
              to={post.slug}
              className="text-blue-600 underline"
            >
              {post.title}
            </Link>
          </li>
        ))}
      </ul>
    </main>
  );
}

아마 TypeScript가 잔뜩 화가 나서 에러를 쏟아내고 있을 거에요. 좀 진정시켜줍시다.

app/routes/posts/index.tsx

import { json } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";

type Post = {
  slug: string;
  title: string;
};

type LoaderData = {
  posts: Array<Post>;
};

export const loader = async () => {
  return json<LoaderData>({
    posts: [
      {
        slug: "my-first-post",
        title: "My First Post",
      },
      {
        slug: "90s-mixtape",
        title: "A Mixtape I Made Just For You",
      },
    ],
  });
};

export default function Posts() {
  const { posts } = useLoaderData() as LoaderData;
  return (
    <main>
      <h1>Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.slug}>
            <Link
              to={post.slug}
              className="text-blue-600 underline"
            >
              {post.title}
            </Link>
          </li>
        ))}
      </ul>
    </main>
  );
}

어때요, 멋지지 않나요? 모든 것이 동일한 파일에 정의되어 있기 때문에 네트워크 요청에서도 어느 정도 확실한 타입 안전성을 얻을 수 있습니다. Remix가 데이터를 가져오는 사이에 네트워크가 끊어지지 않는 한, 컴포넌트와 해당 API에 타입 안전성이 있습니다(컴포넌트가 이미 자체 API 경로임을 기억하세요).

약간의 리팩토링

리팩토링을 하는 최고의 방법은 특정 문제만 다루는 모듈을 만드는 것입니다. 이 튜토리얼의 경우에는 게시물을 읽고 쓰는 기능이 필요하죠. 자, 그럼 export한 getPosts를 모듈에 추가하고 설정해보겠습니다.

💿 app/models/post.server.ts 파일 만들기

touch app/models/post.server.ts

보통 기존 route에서 복사/붙여넣기를 합니다:

app/models/post.server.ts

type Post = {
  slug: string;
  title: string;
};

export async function getPosts(): Promise<Array<Post>> {
  return [
    {
      slug: "my-first-post",
      title: "My First Post",
    },
    {
      slug: "90s-mixtape",
      title: "A Mixtape I Made Just For You",
    },
  ];
}

getPosts 함수를 비동기식으로 만들고 있다는 점에 유의하세요. 설령 현재 비동기식으로 실행되지 않는 것처럼 보여도요.

💿 새로운 posts 모듈을 사용하도록 posts routes 업데이트하기

app/models/post.server.ts

import { json } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";

import { getPosts } from "~/models/post.server";

type LoaderData = {
  // this is a handy way to say: "posts is whatever type getPosts resolves to"
  posts: Awaited<ReturnType<typeof getPosts>>;
};

export const loader = async () => {
  return json<LoaderData>({
    posts: await getPosts(),
  });
};

데이터 소스(data source)에서 가져오기

Indie Stack을 사용하면 SQLite 데이터베이스가 이미 설정 및 구성되어 있으므로 SQLite를 처리하도록 데이터베이스 스키마를 업데이트만 하면 됩니다. Prisma를 사용하여 데이터베이스와 상호 작용하므로, 스키마를 업데이트하고 Prima는 스키마에 맞게 데이터베이스를 업데이트 처리해야 합니다. 여기서 말하는 처리란 마이그레이션에 필요한 SQL 명령을 생성하고 실행하는 것을 말합니다.

Remix를 사용할 때 꼭 Prisma를 사용할 필요는 없습니다. Remix는 현재 상용중인 모든 데이터베이스 혹은 데이터 영속성 서비스와 잘 맞습니다.

만약 Prisma를 사용해본 적이 없으셔도 괜찮습니다. 천천히 같이 해봅시다.

💿 프리즈마 스키마(Prisma schema) 업데이트 하기

prisma/schema.prisma

// Stick this at the bottom of that file:

model Post {
  slug     String @id
  title    String
  markdown String

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

💿 이제 Prisma에게 저희의 로컬 데이터베이스와 TypeScript 정의를 이 스키마 변경과 일치하도록 업데이트하라고 지시해 봅시다:

npx prisma db push

💿 몇 개의 게시물로 데이터베이스를 시드(seed)*합시다. prisma/seed.ts 파일을 열고 아래 코드를 function seed()의 끝부분에 추가하세요(console.log 바로 앞에 넣으면 됩니다).

*seeding: 프로젝트 초기에 테스트용으로 더미 데이터를 넣는 것을 말합니다.

prisma/seed.ts

const posts = [
  {
    slug: "my-first-post",
    title: "My First Post",
    markdown: `
# This is my first post

Isn't it great?
    `.trim(),
  },
  {
    slug: "90s-mixtape",
    title: "A Mixtape I Made Just For You",
    markdown: `
# 90s Mixtape

- I wish (Skee-Lo)
- This Is How We Do It (Montell Jordan)
- Everlong (Foo Fighters)
- Ms. Jackson (Outkast)
- Interstate Love Song (Stone Temple Pilots)
- Killing Me Softly With His Song (Fugees, Ms. Lauryn Hill)
- Just a Friend (Biz Markie)
- The Man Who Sold The World (Nirvana)
- Semi-Charmed Life (Third Eye Blind)
- ...Baby One More Time (Britney Spears)
- Better Man (Pearl Jam)
- It's All Coming Back to Me Now (Céline Dion)
- This Kiss (Faith Hill)
- Fly Away (Lenny Kravits)
- Scar Tissue (Red Hot Chili Peppers)
- Santa Monica (Everclear)
- C'mon N' Ride it (Quad City DJ's)
    `.trim(),
  },
];

for (const post of posts) {
  await prisma.post.upsert({
    where: { slug: post.slug },
    update: post,
    create: post,
  });
}

참고: 위의 코드처럼 upsert를 사용하면 게시물을 올릴 때마다 여러 버전을 추가하지 않고도 시드 스크립트를 계속해서 실행할 수 있습니다.

좋습니다. 시드 스크립트를 사용해서 해당 게시물을 데이터베이스에 가져옵시다.

npx prisma db seed

💿 스키마 변경을 위해 마이그레이션 파일을 생성하겠습니다. 이는 로컬에서 개발 모드로 실행할 때보다는 애플리케이션을 배포하는 경우에 필요한 과정입니다.

npx prisma migrate dev

마이그레이션 이름을 지정할 수 있는 기능이 제공되며, 변경 내용을 다시 참조할 수 있으므로 이름에 대해 post 모델을 만드는 것을 추천드립니다.

💿 이제 SQLite 데이터베이스에서 읽도록 app/delay/post.server.ts 파일을 업데이트하세요.

app/models/post.server.ts

import { prisma } from "~/db.server";

export async function getPosts() {
  return prisma.post.findMany();
}

return 타입을 제거해도, 여전히 모든 타입이 완벽히 입력(fully typed)되어 있는 것을 주목해주세요. 이처럼 수동으로 타입을 입력하는 것은 줄이면서도, 타입 안정성을 확보할 수 있다는 점은 Prisma가 가진 최고의 장점 중 하나입니다.

import문의 ~/db.server는 app/db.server.ts에서 파일을 가져오는 것을 뜻합니다. ~는 app 디렉토리를 부르는 세련된 방식으로, 더이상 ../../s로 번거롭게 쓰시지 않으셔도 됩니다. import 할 때마다 .의 갯수가 틀릴까봐 걱정하지 않으셔도 된다는 뜻이에요!

💿 Prisma 클라이언트가 업데이트되었으므로 서버를 다시 시작해야 합니다. dev 서버를 중지하고 npm run dev로 다시 시작하세요.

Prisma 스키마를 변경하고 Prisma 클라이언트를 업데이트할 때만 이 작업을 수행하면 됩니다. 일반적으로 개발 중에 개발 서버를 다시 시작할 필요가 없습니다. 이정도면 꽤 빠르지 않나요?

서버가 가동되고 다시 실행되면 http://localhost:3000/posts로 이동할 수도 있고, 게시물도 여전히 제 자리에 있을 거에요. 하지만 이제는 이 모든 것들이 SQLite에서 가져오는 거랍니다!

🌟

Pulling from a data source 챕터까지 완료되었습니다. 번역하려니 생각보다 글이 많이 길어서, 여기서 끊고 다음 챕터부터는 2편으로 뵙겠습니다.

profile
공부하는 즐거움

0개의 댓글