Remix 문서 리뷰 (v2.4.0)

share, study, company · 2023-12-27

← 리스트로

Remix 문서 리뷰 (v2.4.0)

튜토리얼

기본 페이지

  • app/routes/_index.tsx

스타일 시트 가져오기

  • LinksFunction 을 이용해서 css파일을 내보내면 스타일이 적용된다.
import type { LinksFunction } from "@remix-run/node";
// existing imports

import appStylesHref from "./app.css";

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

라우터 연결

  1. routes 밑에 해당 파일(contacts.$contactId.tsx)을 만들면 http://localhost:3000/contacts/1 과 같이 경로와 연결 할 수 있다. ($가 붙으면 변수처럼 동작)
  2. contacts.\$contactId_.edit.tsx 라는 파일의 뒤 언더바는 앞에서 정한 contacts.$contactId.tsx 파일의 경로는 중첩되지만 레이아웃 상에서는 중첩되지 않는다는걸 의미한다.
  3. Link 를 가져와서 a 태그 대신 쓰도록 변경해준다.
  4. <Outlet />은 하위 경로 페이지에 해당하는 컴포넌트를 그려준다.

데이터 가져오기

  • 데이터를 가져오려면 두가지 api를 사용해야 한다. (useLoaderDataloader)
  1. 데이터를 리턴하는 loader함수를 만들고 내보낸다.
  2. 컴포넌트에서 useLoaderData<typeof loader>()를 이용해 데이터를 가져온다.

Loader 에서 params 사용하기

  • 페이지 경로의 contactId 와 $contactId 를 매칭하여 loader의 params 객체를 통해 값을 가져올 수 있다.
export const loader = async ({ params }) => {
  const contact = await getContact(params.contactId);
  return json({ contact });
};
  • loader 함수의 파라미터의 타입은 아래처럼 가져올 수 있다.
import type { LoaderFunctionArgs } from "@remix-run/node";

import invariant from "tiny-invariant"; //사용자의 값에 문제가 있을 경우 예외처리 해준다.

export const loader = async ({
  params,
}: LoaderFunctionArgs) => {
  invariant(params.contactId, "Missing contactId param");
  const contact = await getContact(params.contactId);
  if (!contact) {
    throw new Response("Not Found", { status: 404 });
  }
  return json({ contact });
};

Form 과 submit

  • 밑의 예제에서 Edit 버튼을 누르면 현재 페이지의 /edit 경로의 path로 이동한다.
  • 마치 전통적인 브라우저의 form 태그처럼 동작한다.(Remix가 인터페이스만 흉내냈다)
  • 만약 Javascript가 허용안된 페이지에서는 전통적인 FormData가 동작하는 방식대로 실행된다.
  • From 컴포넌트에도 onChange 이벤트를 걸수 있다.
<Form action="edit">
    <button type="submit">Edit</button>
</Form>
  • useActionData를 사용하면 액션에 대한 서버의 응답을 컴포넌트에 전달받을 수 있다.
  • action이 생략되면 해당 페이지에서 내보내기한 action 함수가 실행된다.
  • 완성후 다른페이지로 리다이랙트 시킴 (리다이렉트는 히스토리에 안남기고 페이지를 이동시킨다)
import type { ActionFunctionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";

import { getContact, updateContact } from "../data";

export const action = async ({
  params,
  request,
}: ActionFunctionArgs) => {
  invariant(params.contactId, "Missing contactId param");
  const formData = await request.formData();
  const updates = Object.fromEntries(formData);
  await updateContact(params.contactId, updates);
  return redirect(`/contacts/${params.contactId}`); 
};

NavLink

  • Link 대신 사용할수 있으면 현재 url 과 일치하는 to가 있으면 isActive를 반환해준다.
<NavLink
  className={({ isActive, isPending }) =>
    isActive ? "active" : isPending ? "pending" : ""
  }
  to={`contacts/${contact.id}`}
>

useNavigation

const navigation = useNavigation(); navigation.state

  • 현재 페이지의 로딩 상태를 반환한다.
    • idle
    • loading
    • submitting
  • navigation.location 속성은 loading 중일때만 값을 가지고 있다.

useNavigate

  • 백버튼 사용 예
const navigate = useNavigate();

return (
    <button => navigate(-1)} type="button">
      Cancel
    </button>
);

url의 쿼리스트링에서 정보 가져오기

  • request.url 로 가져와서 아래처럼 직접 파싱한다.
  • Form에 메서드가 get일 경우 submit되도 액션은 일어나지 않는다.
export const loader = async ({
  request,
}: LoaderFunctionArgs) => {
  const url = new URL(request.url);
  const q = url.searchParams.get("q");
  const contacts = await getContacts(q);
  return json({ contacts });
};

useSubmit

  • 인수에 Form에 해당하는 DOM 을 넣고 함수를 실행시킬 수 있다.
  • submit이 되면 히스토리에 쌓인다
  • 두번째 옵션의 객체에 replace 옵션을 주면 히스토리에 쌓이지 않게 할 수 있다.

useFetcher

  • useFetcher 를 사용하면 navigation에 영향을 주지않고 loader나 action과 통신할수 있다.
  • useFetcher 를 사용하려면 액션과 함께 사용해야 한다.
// existing imports
import {
  Form,
  useFetcher,
  useLoaderData,
} from "@remix-run/react";
// existing imports & exports

// existing code

const Favorite: FunctionComponent<{
  contact: Pick<ContactRecord, "favorite">;
}> = ({ contact }) => {
  const fetcher = useFetcher();
  const favorite = contact.favorite;

  return (
    <fetcher.Form method="post">
    ...
    </fetcher.Form>
  );
};

소개

  • 리믹스는 핸들러일뿐이므로 Vercel , Netlify , Architect 등과 같은 모든 Node.js 서버는 물론 Cloudflare Workers 및 Deno Deploy 와 같은 Node.js가 아닌 환경 에서도 실행할 수 있습니다.
  • 리믹스는 “뷰” 이자 "컨트롤러"다. 모델 구축은 사용자에게 달려있다.
  • 경로에 UI와 모델과 상호작용이 모두 포함된다.
  • 라우터 모듈은 loader, action, default 의 세 가지를 내보낼 수 있다.
    • loader 는 서버에서만 실행되며 데이터를 요청한다.
    • default 는 기본 내보내기이며 경로가 일치될때 렌더링된다.
      • 컴포넌트는 중첩되는 하위 경로를 렌더링한다.
    • action은 서버에서만 실행되며 post를 처리한다.
  • 리믹스의 라우터는 하나의 api 경로다.
  • 리믹스 템플릿을 사용하면 다양한 환경에서 시작하기 좋은 개발 환경 보일러플레이트를 제공받을수 있다.
  • 리믹스 클라이언트는 자바스크립트 없이 동작할 수 있도록 개발되었다(브라우저에 자바스크립트사용을 비활성화 시켜도 기본적인 동작을 한다.)
  • 점진적인 향상 기법을 이용해 미리 페이지를 로드하므로 페이지 체감속도가 빠르다.

경로 구성

  • app/routes/ 의 경로가 기본 컨트롤러의 의 경로다.

  • 기본 규칙은 듀토리얼에서 학습한 내용이 전부지만 폴더링을 하여 스캐폴딩 하고 싶을경우 아래의 예처럼 하면 된다

    app/
    ├── routes/
    │   ├── _index/
    │   │   ├── signup-form.tsx
    │   │   └── route.tsx
    │   ├── about/
    │   │   ├── header.tsx
    │   │   └── route.tsx
    │   ├── concerts/
    │   │   ├── favorites-cookie.ts
    │   │   └── route.tsx
    │   ├── concerts.$city/
    │   │   └── route.tsx
    │   ├── concerts._index/
    │   │   ├── featured.tsx
    │   │   └── route.tsx
    │   └── concerts.trending/
    │       ├── card.tsx
    │       ├── route.tsx
    │       └── sponsored.tsx
    └── root.tsx
    
  • 세그컨트를 괄호로 묶으면 선택적 세그먼트가 된다 (있어도 되고 없어도 되는 세그먼트)

    • /categories -> app/routes/($lang).categories.tsx
    • /en/categories -> app/routes/($lang).categories.tsx
  • remix.config.js 에 규칙을 정의하면 수동으로 경로를 구성할수 있다.

    /** @type {import('@remix-run/dev').AppConfig} */
    export default {
      routes(defineRoutes) {
        return defineRoutes((route) => {
          route("/", "home/route.tsx", { index: true });
          route("about", "about/route.tsx");
          route("concerts", "concerts/layout.tsx", () => {
            route("", "concerts/home.tsx", { index: true });
            route("trending", "concerts/trending.tsx");
            route(":city", "concerts/city.tsx");
          });
        });
      },
    };
    

데이트 흐름

  • Loader -> Component -> Action

클라이언트빌드

  • 클라이언트 빌드에서는 아래의 서버 전용 코드가 제거된다.
    • action
    • headers
    • loader
  • *.client.tsx*.server.tsx를 사용하면 특정 코드를 클라이언트나 서버빌드에서 뺄수 있다.

보류중인 UI

  • 아래 기능들을 이용하면 보류중인 UI를 고려하여 더 좋은 사용자 경험 페이지를 만들 수 있다.
    • useNavigate
    • useFetcher
    • useParam
      • NavLink

상태관리

  • Redux, ReactQuery, Apollo 와 같은 상태관리 툴들이 유용하긴 하지만 Remix와 함께 사용하기에는 적합하지 않다.
  • 대부분이 Remix에서 클라이언트 데이터를 캐싱해주고 있는 부분과 기능이 중복된다고 한다.
  • 대부분의 상태들이 이미 Remix에서 제공하는 기능에 상태에 저장된다.
  • Remix에서 제공하는 상태관리 이외에 상태를 관리해야 할때는 로컬저장소나 쿠키를 활용하는걸 추천한다.
  • useOutletContext 정도로 충분한것 같다.

*.client.ts

  • 해당 이름으로 정의된 파일 내에서 export 하면 클라이언트에서만 쓸수 있습니다.
    • useEffect나 이벤트 핸들러 내에서만 접근 가능합니다.

*.server.ts

  • 서버 종속성이 있는 파일은 이와같이 정의하면 클라이언트 빌드에서는 제거됩니다.

entry.client.tsx

  • Remix는 자체적으로 알아서 클라이언트를 위한 수화처리를 하는데 remix reveal 을 사용하면 이 동작을 커스텀 변경 할수 있다.
  • revial을 하면 entry.client.tsx이 생성되느데 이 파일을 원하는대로 수정하면 된다.

entry.server.tsx

  • http 응답을 후킹하여 수정할 수 있습니다.
  • revial을 하면 entry.server.tsx이 생성되느데 이 파일을 원하는대로 수정하면 된다.

remix.config.js

  • 각종 Remix의 환경설정

root

  • 루트에서만 사용해야되는 기능이 있다.
import {
  Links,
  LiveReload,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";

action

  • DELETE, PATCH, POST 또는 PUT 요청이 일어날 경우 loader 보다 action 이 우선 실행된다.
  • loader와 action은 똑같은 구성이지만, action은 쓰는데이터에 loader는 데이터를 읽는데 쓰인다.
  • Form의 액션값을 생략하면 자기자신의 action을 실행하고, 액션에 path가 정의되어 있으면 해당 경로의 action을 실행한다.

clientAction

  • 클라이언트에서만 실행되는 액션을 정의할 수 있다.
  • 액션 실행시 클라이언트에서는 다른 추가 기능이 필요할때 사용

clientLoader, HydrateFallback

  • clientAction과 마찬가지로 클라이언트에서만 실행되는 로더다.
  • 페이지 첫 로드시 하이드레이션 될 때는 clientLoader가 실행되지 않는다.
    • (꼭 필요한 경우에는 clientLoader.hydrate 를 통해 설정가능하다)
  • clientLoader에서 가져와야 하는 데이터가 있기 때문에 SSR 중에 기본 경로 구성 요소를 렌더링하지 않아야 하는 경우 HydrateFallback 구성 요소를 사용할 수 있다.

ErrorBoundary

  • 오류 처리를 위한 컴포넌트

useMatchs, handle

  • 서브 경로마다 handle을 export 하면 root.ts 에서 useMatchs를 통해 노출되고 있는 라우터를 전부 표시할수 있다.
  • breadcrumb 와 같은 ui 구현시 용이하다.

headers

  • 각 라우터마다 헤더를 http 헤더에 추가 정보를 포함시킬 수 있다.
import type { HeadersFunction } from "@remix-run/node"; // or cloudflare/deno

export const headers: HeadersFunction = ({
  actionHeaders,
  errorHeaders,
  loaderHeaders,
  parentHeaders,
}) => ({
  "X-Stretchy-Pants": "its for fun",
  "Cache-Control": "max-age=300, s-maxage=3600",
});

links

  • root에서 링크 태그로 리소스 삽입

loader

  • loader 응답은 new Response 를 만들어서 리턴할수도 있고
  • import { json, redirect } from "@remix-run/node";를 가져와서 실행할 수 있습니다.

meta

  • links 처럼 meta 태그를 내보낼수 있다.

ShouldRevalidate

  • React의 shouldUpdate 처럼 특정 페이지에 업데이트에 loader가 반응할 필요가 있는 항목만 필터링 할 수 있다.
  • 각 항목의 설명은 필요시 찾아보면 되므로 패스
import type { ShouldRevalidateFunction } from "@remix-run/react";

export const shouldRevalidate: ShouldRevalidateFunction = ({
  actionResult,
  currentParams,
  currentUrl,
  defaultShouldRevalidate,
  formAction,
  formData,
  formEncType,
  formMethod,
  nextParams,
  nextUrl,
}) => {
  return true;
};

컴포넌트

  • 지연되는 Promise를 기다려 줬다 보여주는 기능을 합니다.
import { Await } from "@remix-run/react";

<Suspense fallback={<div>Loading...</div>}>
  <Await resolve={somePromise}>
    {(resolvedValue) => <p>{resolvedValue}</p>}
  </Await>
</Suspense>;

  • URL을 변경하거나 브라우저 히스토리 스택을 변경해야할때 사용하는 HTML 래퍼다.
  • 브라우저 스택을 조작하지 않아야 할때는 <fetcher.Form> 을 사용하면 된다.
  • 속성
    • action - 듀토리얼에서 실습해봤음
    • method - 듀토리얼에서 실습해봤음
    • encType - application/x-www-form-urlencoded or multipart/form-data.
    • navigate - <fetcher.Form> 와 비슷하지만 결과에 신경쓰지 않고 데이터 변경이 필요할수 있으므로 사용된다.(UI와의 결합도를 낮춰줌)
    • fetcherKey - useFetchers와 함께 사용할때 유용하게 사용할수 있다.
    • preventScrollReset - from을 재출할때 스크롤이 상단으로 재설정 되는걸 방지 한다.
    • replace - 새 항목을 푸시하는대신 기존 마지막 항목의 상태를 바꾼다.
    • reloadDocument - 리얼 브라우저의 From 기능을 사용한다.

  • a tag 처럼 페이지를 이동시키지만 라우팅 기능으로 동작한다.
  • 속성
    • prefetch - none, intent(마우스를 올리거나 포커스될때), render(랜더링될때), viewport(뷰포트에 올라올때)
    • relative - 링크의 상대경로 동작을 정의
    • reloadDocument - 리얼 브라우저의 a 기능을 사용합니다.
    • replace - replace은 새 항목을 푸시하는 대신 기록 스택의 현재 항목을 대체합니다.
    • state - 클라우트측 라우팅 상태를 추가

  • 라우터에 등록된 모든 링크를 출력

  • 개발중이던 파일이 변경되면 자동으로 리로드 시켜준다.

  • meta 태그 내보낼때 문서에서 태그 위치 지정

  • 태그와 같지만 스타일을 지정하기 위한 기능이 추가되어 있다.

  • 하위경로 렌더링
  • context 속성 사용가능 useOutletContext 와 함께 사용가능

  • 이 구성 요소를 사용하면 페이지의 모든 자산을 프리페치하여 해당 페이지를 즉시 탐색할 수 있습니다.

  • 스크립트 랜더링

  • location 변경시 스크롤을 복원해준다.

useActionData

  • 가장 최근 작업한 action 데이터를 사용할수 있게 해준다.

useAsyncError

  • Await 컴포넌트와 같이 쓰이며 에러처리를 위해 쓰인다.

useAsyncValue

  • Await 컴포넌트와 같이 쓰이며 에러처리를 위해 쓰인다.

useBeforeUnload

  • 페이지에서 떠날때 어플리케이션의 상태를 정리해주는건 좋은 습관이다.

useBlocker

  • 사용자가 현재 위치에서 벗어나는 것을 방지하고 사용자 지정 UI를 제공

useFetcher

  • 서버와 상호작용을 하기 위한 훅
  • key 속성은 useFetchers에서 조회될경우 식별할 수 있게 해준다.
  • <fetcher.Form> 과 함께 사용 가능
  • fetcher.submit 을 사용할수 있다. useSubmit의 fetcher 버전인듯 하다.
  • fetcher.load 특정 경로의 loader에서 데이터를 로드하고 해당 UI에 업데이트 된다.
  • 읽기 속성들
    • state - fetcher의 상태 (요청중인지 로딩중인지 대기중인지)
    • data - 작업에서 반환된 데이터가 여기에 저장됨
    • formData - 서버에 제출된 폼데이터의 인스턴스가 저장됨
    • formAction - 제출된 액션 url
    • formMethod - 제출된 폼의 제출방식

useFetchers

  • 모든 진행중인 fetcher의 배열을 반환합니다.

useFormAction

  • 가장 가까운 라우터 경로를 반환함으로서 action 경로를 쉽게 만들 수 있게 해 준다.

useHref

  • 링크 경로로 사용할 현재 위치에 대한 전체 url 을 리턴한다.

useLoaderData

  • loader 부터 데이터를 컴포넌트로 가져온다.

useLocation

  • 현재 location 객체를 반환한다.

useMatches

  • 페이지에서 현재 경로와 일치하는 경로들을 리턴한다. handle과 사용하면 편리하게 사용할수 있다.

useNavigate

  • 듀토리얼 포함내용이라서 설명생략.

useNavigation

  • 듀토리얼 포함내용.
  • pending 상태의 페이지 요청에 진행 상태를 리턴해준다.

useNavagationType

  • 현재 위치의 페이지에 도달했을때 탐색 유형을 반환한다.
  • (push, replace, pop)

useOutlet

  • 하위계층에 대한 요소를 반환한다.
  • 컴포넌트에서 내부적으로 사용되는 훅.

useOutletContext

  • 가장 가까운 상위 컨텍스트(<Outlet context="값" />으로 정의한) 값을 반환한다.

useParams

  • 현재 url에서 쿼리스트링 값을 키-값 객체 형태로 리턴

useResolvePath

  • 알고자 하는 라우터 경로에 해당하는 path정보를 리턴한다.

useRevalidator

  • 페이지의 유효성을 검증한다 (특수한 경우에만 유용하며 일반적인 상황에서는 쓸 필요 없다).
  • 일반적인 흐름으로 제공되는 action , loader, form 에 연결된 api에서는 자체적으로 이미 검증을 하고 있으므로 사용할 필요 없다.

useRouterError

  • action, loader 사용중에 발생하는 오류를 처리할 수 있다.

useRouteLoaderData

  • 특정 라우터에 대한 loader 데이터에 접근할 수 있다.
  • fetcher.load와 다른점은 UI와 상호 작용 하는 방식이 틀린것 같다.
    • (현재 문서에 자세히 설명이 안되어 있어서, 자세히 동작을 알아보기 위해서는 직접 사용하며 테스트를 할 필요가 있어보인다.)

useSearchParams

  • 현재 url에 searchParams와 이를 변경할 수 있는 세터를 반환한다.

useSubmit

  • 을 스크립트로 submit 해줄수 있다.

유틸리티

쿠키 createCookie

  • loader 및 action에서 사용할수 있다.
  • (서버측에서 쿠키를 만들때 사용하고 클라이언트에서는 별도의 api는 없는것 같다.)
// *.server.ts
import { createCookie } from "@remix-run/node"; // or cloudflare/deno

export const userPrefs = createCookie("user-prefs", {
  maxAge: 604_800, // one week
});

createRemixStub

  • 컴포넌트 단위테스트 용도로 사용한다.

defer

  • 데이터 스트리밍과 관련된 기능, json()과 사용법은 비슷하지만 Promise를 컴포넌트로 직접 전송하는 기능이다.

isRouteErrorResponse

  • react router 에서 라우터 에러 처리와 동일하다고 한다.

json()

  • Response 를 json 형태로 보낼때의 쇼트컷 헬퍼
import { json } from "@remix-run/node"; // or cloudflare/deno

export const loader = async () => {
  // So you can write this:
  return json({ any: "thing" });

  // Instead of this:
  return new Response(JSON.stringify({ any: "thing" }), {
    headers: {
      "Content-Type": "application/json; charset=utf-8",
    },
  });
};

redirect

  • 응답 30x 로 Response를 보내는 쇼트컷

redirectDocument

  • redirect의 래퍼.
  • remix인 앱에서 동일한 도메인의 remix가 아닌 앱으로 리다이렉트 할때 필요하다.

sessions

  • 일련의 방법을 통해 세션처리를 할수 있다- 자세한 사용법은 공식문서 참고.

  • createSessionStorage

    • 데이터를 서버에 저장한다.
    • api를 사용하면 필요한 경우 데이터 베이스에 세션정보를 저장하고 여러 서버에서 공유할수 있다.
  • createCookieSessionStorage

    • 순수 쿠키 기반으로 세션기능을 사용한다. 모든 정보를 쿠키에 저장한다.
  • createMemorySessionStorage

    • 서버 메모리에 세션정보를 저장하지만, 개발용도로만 사용하고 프러덕션에는 사용하면 안된다고 한다.
  • createFileSessionStorage

    • 세션정보를 파일에 저장한다.
  • createWorkersKVSessionStorage

    • Cloudflare Workers KV 세션 지원 서버일 경우 사용한다.
  • createArcTableSessionStorage

    • Amazon DynamoDB 지원 세션 지원 서버일 경우 사용한다.

스타일링

css 번들링

  • npm install @remix-run/css-bundle 설치 후 아래와 같이 사용가능 (엄청 신기함)
import { cssBundleHref } from "@remix-run/css-bundle";
import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

export const links: LinksFunction = () => [
  ...(cssBundleHref
    ? [{ rel: "stylesheet", href: cssBundleHref }]
    : []),
  // ...
];

일반적인 css 사용법

  • 해당 라우터에서만 특정 스타일이 적용되어야 할경우 해당 라우터에 links를 export 하면된다.

    • (캐스캐이딩으로 동작한다).
  • 스타일 공유하기 첫번째 방법

    • (공유 되어야 하는 스타일을 하나의 css파일에 몰아 넣고, root.tsx에 export 시키는 방식)
  • 스타일 공유하기 두번째 방법

    • (컴포넌트 별로 css를 import 하고 필요한 기본 스타일은 상위에서 Links를 import 에서 사용한다.)
    • 중첩관리는 Remix에서 알아서 해주는걸로 판단됨.
  • 미디어 쿼리 연결 가능하며 스타일 뿐만 아니라 svg같은 파일도 load가능

css Import

  • config에서 설정 후
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  serverDependenciesToBundle: [
    /^@adobe\/react-spectrum/,
    /^@react-spectrum/,
    /^@spectrum-icons/,
  ],
  // ...
};
  • 아래와 같이 사용 가능
import "./menu-button.css";

export function MenuButton() {
  return <button data-menu-button>{/* ... */}</button>;
}

Styled Components 및 Emotion과 같은 CSS-in-JS 라이브러리를 사용

  • 사용예도 이미 있고 가능은하지만, 중복 랜더링등 성능 문제가 있어서 Remix에서는 사용하지 않는게 좋다고 함.

css 모듈

  • 내장된 css모듈이 있어서 아래와 같이 사용가능함. 다만 컨피그에서 설정이 필요함
import styles from "./styles.module.css";

export const Button = React.forwardRef(
  ({ children, ...props }, ref) => {
    return (
      <button
        {...props}
        ref={ref}
        className={styles.root}
      />
    );
  }
);
Button.displayName = "Button";

Post css

  • config에서 설정하면 사용 가능함.
  • 먼저 관련된 패키지를 설치해야함
  • css 번들링이나 import 를 사용하여 css를 사용할때 작동한다.

Tailwind

  • Remix에서 사용하는 가장 인기있는 스타일링 방법이다.
  • tailwind.config.js 가 root에 있는 경우 자동 작동한다.
  • @import 를 직접 사용하지 말고 @tailwind 지시어를 사용하는걸 추천한다고 함.

Vanilla Extract

  • 타입스크립트를 사용해 css를 만들어주는 라이브러리 사용가능