Remixrun을 시작 하기 위한 가이드

Dev-Yuns
9 min readNov 20, 2022

remixrunnextjs와 유사하게 react를 베이스로 한 풀스택 웹 프레임워크 입니다. react router를 만들었던 팀에서 제작하고 있고 제가 좋아하는 kentcdodds가 참여한 프로젝트이기도 합니다.

remixrun는 표준 웹 API를 많이 사용하기 때문에 웹에서 자바스크립트 기능을 끄더라도 앱이 구동 가능합니다. 또 node 가 아닌 http server 로 컴파일 되기 때문에 node보다 다양한 환경에 배포가능하다는 특징이 있습니다.

이번 포스팅에서는 remix 에 관심있으신 분들이 빠르게 토이 프로젝트를 만들어볼 수 있도록 필요한 최소한의 기능들을 소개하고자 합니다.

You will find that when you start learning Remix, you’ll spend as much time on the MDN docs, if not more, than the Remix docs. We want Remix to help you build better websites even when you’re not using it.

Get better at Remix, accidentally get better at the web.

Route

remixroutes/ 라는 폴더 아래 페이지 컴포넌트들을 배치하면 그 구조에 따라 url을 만드는 파일 베이스 라우팅을 지원합니다. 라우트 구조가 폴더-파일 구조를 기반으로 한다는 점을 제외하면 react-router v6와 사용법은 유사합니다.

routes/login.tsx 이라는 컴포넌트를 만들면 url 에서 /login 으로 접근가능합니다.

Nested routing

중첩된 라우팅을 만들고 싶다면 routes 폴더 아래 새로운 폴더를 만들고 그 안에 컴포넌트를 생성하면 됩니다. 예를 들어 routes/test폴더를 만들고 그 안에 index.tsx 혹은 other.tsx 파일을 만들면 /test 경로는 routes/test/index.tsx 컴포넌트를, /test/other 경로는 routes/test/other.tsx 컴포넌트를 렌더링할 것입니다.

중첩된 라우팅은 react router v6에서 소개된 Outlet의 개념을 사용합니다. 만약 여러 페이지에 사용되는 공통 UI를 사용하고 싶다면 컴포넌트가 속한 폴더와 같은 이름의 컴포넌트를 만들어서 <main><Outlet/></main> 를 리턴해주면 Outlet 자리에 하위 컴포넌트들이 렌더링됩니다.

└── app
├── root.tsx
└── routes
├── posts
│ ├── $postId.tsx
│ ├── list.tsx
└── posts.tsx

위 구조에서 $postId.tsx컴포넌트와 list.tsx 컴포넌트는 posts.tsx 컴포넌트의 <Outlet/> 자리에 렌더링됩니다.

만약 중첩된 라우트 중 특정 페이지는 Oulet을 상속받지 않도록 하고 싶다면 폴더 밖에 . 을 통해 라우트를 정의할 수 있습니다.

└── app
├── root.tsx
└── routes
├── posts
│ ├── replies
│ │ └── $replyId.tsx
│ └── replies.tsx
├── posts.replies.$replyId.edit.tsx
// url은 같지만 posts.tsx나 replies.tsx의 Outlet 자리에 오지 않습니다.
└── posts.tsx

위 파일 구조에서 routes/posts.tsxposts 폴더 내에 있는 컴포넌트들의 공통 상위 컴포넌트로써, 공통으로 사용되는 UI를 정의해둘 수 있습니다. 마찬가지로 routes/posts/replies.tsxroutes/posts/replies 폴더 아래에 있는 컴포넌트의 공통 상위 컴포넌트입니다. 반면 . 을 통해 정의한 posts.replies.$replyId.edit.tsx 컴포넌트는 posts/replies/123/edit 경로로 접근할 수 있지만 posts.tsxreplies.tsxOutlet 에 포함되지 않고 독립적으로 존재합니다.

만약 공통된 UI를 만들기 위해서 컴포넌트들을 한 폴더에 묶어두고 싶지만 폴더 이름을 url에 표시하고 싶지 않은 경우에는 __ 를 앞에 붙여줄 수 있습니다.

└── app
├── root.tsx
└── routes
├── __main
│ └── home.tsx
└── __main.tsx

__main.tsx 컴포넌트는 __main 폴더 아래에 있는 컴포넌트에 공통으로 적용되지만 home.tsx 에 접근할 때는 __main 을 생략하고 /home 으로 접근할 수 있습니다.

Dynamic Routing

블로그 포스트 아이디와 같이 동적으로 변하는 id 값을 통해 라우터를 만들고 싶다면 $ 키워드와 함께 사용할 수 있습니다. routes/posts/폴더 아래 $postId.tsx 와 같이 작성한 컴포넌트는 /posts/123 으로 접근할 수 있고, 컴포넌트 내부에서 useParams 로 접근할 수 있습니다.

Dynamic Route 안에서 다시 Dynamic Route 를 중첩해서 만들 수도 있습니다. 예를 들어 $postId 라는 폴더를 만들고 그안에 $replyId.tsx 컴포넌트를 만든다면 /posts/123/abc 와 같이 접근할 수 있습니다. 컴포넌트 내부에서는 params.postId , params.replyId 와 같이 URL param 에 접근할 수 있습니다.

Style

remix 에서는 페이지 별로 편리하게 stylesheet 를 로드할 수 있는 기능이 있습니다. styles.css 에 정의한 클래스는 해당 컴포넌트에서 사용할 수 있습니다.

import type {
LinksFunction,
} from '@remix-run/node';
import {
Links,
Outlet,
} from '@remix-run/react';
import styles from "./styles.css";

export const links: LinksFunction = () => [
{ rel: "stylesheet", href: styles },
];

export default function App() {
return <Outlet/>
}

만약 스타일시트를 root.tsx 에서 import 했다면 전체 컴포넌트에서 해당 스타일시트에 정의된 클래스를 사용할 수 있습니다.

Data load

각 컴포넌트에서는 loader 를 통해 서버에서 데이터를 받아올 수 있습니다.

type LoaderData = {
data: string;
}

export const loader: LoaderFunction = ({request}) => {
//
return json<LoaderData>({data: 'hi'})
}

export default function Home() {
const {data} = useLoaderData<LoaderData>();

return (
<div>{data}</div> // hi
)
}

loaderget 요청이 들어왔을 때 서버에서 런타임에 실행되며 useLoaderData 를 통해 데이터를 받아올 수 있습니다. loader 에서 리턴하는 json 함수는 서버에서 생성된 데이터를 json 형태로 클라이언트에서 받아볼 수 있도록하는 유틸 함수입니다.

Data mutation

remix 의 특징은 post 요청을 위해서 별개의 JS 라이브러리를 사용하는 것이 아닌 Web Fetch Api를 활용하고 있다는 점입니다.

Form 을 통해 제출된 요청은 action 함수에서 표준 request 객체를 통해 받아서 처리됩니다. action 함수는 서버에서만 동작하며, 표준 response 객체를 리턴해줍니다. 처리 결과에 따라 remix에서 제공하는 redirect 함수 등을 활용할 수 있습니다.

그런데 Form을 사용해서 post 요청을 보내게 되면 전체 페이지 리로드가 일어납니다. 만약 이런 동작을 원치 않는 경우 useFetcher 훅을 사용할 수 있습니다.

useFetcher 에서 제공하는 Form 을 사용하면 전체 페이지 리로드 없이 요청을 보낼 수 있습니다. fetcher.Formaction에는 미리 정의해놓은 action 함수의 경로를 넣어줍니다. 만약 action 에 경로를 넘겨주지 않는다면 Form 은 해당 컴포넌트에서 있는 action 함수로 요청을 보낼 것입니다.

Error handling

remix 의 또다른 편리한 점은 프레임워크 단에서 제공되는 ErrorBoundary 를 사용해서 손쉽게 에러를 처리할 수 있다는 점입니다.

// root.tsx

export const ErrorBoundary: ErrorBoundaryComponent = ({error}) => {
if (process.env.NODE_ENV === 'development') {
console.error('error', error);
}

return (
<div>
<h1>
{error.stack} {error.message}
</h1>
</div>
);
};

export const CatchBoundary: CatchBoundaryComponent = () => {
const caught = useCatch();

return (
<div>
<h1>
{caught.status} {caught.statusText}
</h1>
<p>{caught.data}</p>
</div>
);
};

root.tsx 에서 ErrorBoundaryCatchBoundary 를 정의해놓으면 앱 전체에서 발생한 에러에 대응할 수 있습니다. 물론 컴포넌트 별로 범위를 축소해서 정의하면, 에러가 발생한 지점에서 가장 가까운 컴포넌트의 ErrorBoundary 혹은 CatchBoundary 를 보여줄 것입니다.

ErrorBoundaryCatchBoundary차이는 핸들링할 수 있는 에러의 인가입니다. ErrorBoundary 는 네트워크 에러와 같이 직접 핸들링하기 힘든 에러가 발생할 때 실행되며, CatchBoundaryloader 혹은 action 함수에서 try...catch 문으로 핸들링 가능한 에러에 대응해줍니다.

Remix 는 사용법이 직관적이고 쉬워서 우선 가벼운 프로젝트에 빠르게 도입해보기 좋은 것 같습니다. 아직 Nextjs 에 비해 개발 생태계가 부족하지만 발전이 기대되는 프로젝트라는 생각이 듭니다. 참고용 remix 보일러플레이트 코드가 필요하시다면 링크에서 확인할 수 있습니다.

참고하면 좋은 글

--

--