약 6개월 전, 저는 회사 웹 애플리케이션의 기반으로 Remix를 선택하여 일부 사람들이 말하는 대담한 결정을 내렸습니다. 오늘로 돌아가서, 이제 한발 물러서서 우리가 내린 선택을 되돌아볼 때라고 생각합니다. 주요 인프라 결정을 검토하고 그 과정에서 약간의 실제 사용 사례를 뿌릴 예정입니다.
자, 더 이상 고민하지 말고, 만족감과 교훈이 혼합된 이번 여정의 하이라이트와 로우라이트를 바로 살펴보겠습니다.
이것은 아마도 당시 제가 내린 "가장 위험한" 인프라 결정일 것입니다. 왜냐하면 Remix는 NextJS만큼 인기가 없었고 제가 아는 한 대기업에서 Remix를 사용하는 사례도 많지 않았기 때문입니다.
오늘로 빠르게 이동하세요. ChatGPT가 불과 며칠 전에 Next에서 Remix로 마이그레이션되었습니다!
이전 기사에서 자세히 설명했듯이 저는 여러 가지 이유로 Remix를 선택했습니다. 그 중 일부는 단순성, "풀 스택" 측면(즉, 리믹스 서버를 "프론트엔드용 백엔드"로 활용) 및 훌륭한 추상화 때문이었습니다. 라우팅, 데이터 가져오기 및 변형.
다행히 리믹스가 배송됐나요? 프레임워크는 직관적이고 배우기 쉽고 다른 사람에게 가르치기 쉬우며 모범 사례가 사용되도록 보장하므로 코드 작성과 테스트가 모두 간단해집니다.
Remix를 사용한 지 몇 달 후 그들은 React Router와의 공식 합병을 발표했습니다. Vite로의 전환처럼 더 많은 사람들이 이를 사용하도록 설득할 수 있기를 바랍니다.
리믹스가 올바른 선택이라는 것이 여러 번 분명해졌습니다. 최근에 제가 다루었던 실용적인 예를 하나 들어 보겠습니다. 리믹스 서버의 단일 로거 인스턴스를 사용하여 전체 앱에 걸쳐 작업과 오류를 기록하고 추적하여 모니터링 능력을 향상시킬 수 있습니다. 구현은 매우 간단했습니다.
1단계 - 로거 만들기(저의 경우 모니터링에 사용하는 Datadog과 잘 작동하는 Winston을 사용했습니다.)
2단계 - 서버의 로드 컨텍스트에 로거를 추가합니다(제 경우에는 명시적이었습니다).
app.all( '*', createRequestHandler({ getLoadContext: () => ({ logger, // add any other context variables here }), mode: MODE, // ... }), );
3단계(typescript 사용자의 경우) - 앱 로드 컨텍스트에 로거를 포함하도록 Remix의 기본 유형 정의를 업데이트합니다
import '@remix-run/node'; import { type Logger } from 'winston'; declare module '@remix-run/node' { interface AppLoadContext { logger: Logger; } }
4단계 - 모든 경로의 로더 또는 작업에서 원하는 대로 로거를 사용하세요!
export async function action({ request, context }: ActionFunctionArgs) { try { await someAction(); } catch (e) { context.logger.error(e); } }
이 섹션을 마무리하기 전에 스트리밍 데이터/구성 요소를 위한 RSC 구현 및 인증에 유용한 경로 미들웨어와 같이 Remix에 있었으면 좋겠지만 아직 없는 기능도 있다는 점을 말씀드리고 싶습니다. /권한 부여. 다행히도 이러한 기능(및 기타 멋진 기능)이 로드맵에서 우선순위에 있는 것으로 보이므로 곧 출시될 수 있기를 바랍니다!
과거의 긍정적인 경험을 바탕으로 @tanstack/react-query를 선택하는 것은 쉬운 결정이었으며 이번에도 실망하지 않았습니다. API는 다재다능하고 확장 가능하며 최선의 방법으로 편견이 없으므로 다른 도구와 쉽게 통합할 수 있습니다.
아폴로 클라이언트라는 더 확실한 선택 대신 내부 API가 GraphQL 기반이라는 것을 알고 너무 좋아서 선택했습니다. 그 이유에는 여러 가지 이유가 있습니다. Tanstack Query는 뛰어난 API를 가지고 있고 Apollo보다 훨씬 가벼우며, 필요할 경우를 대비해 GraphQL과 같은 특정 기술에 크게 맞춰진 도구에 의존하고 싶지 않았기 때문입니다. 다른 기술을 전환하거나 통합하세요.
게다가 Remix를 사용하고 있기 때문에 Tanstack Query의 SSR 기능을 완전히 활용할 수 있습니다. 즉, 클라이언트 측에서 이러한 쿼리를 변경, 무효화 또는 다시 가져오는 기능을 계속 유지하면서 서버 측에서 쿼리를 프리페칭할 수 있습니다. 다음은 간단한 예입니다.
import { dehydrate, QueryClient, HydrationBoundary, useQuery } from '@tanstack/react-query'; import { json, useLoaderData } from '@remix-run/react'; const someDataQuery = { queryKey: ['some-data'], queryFn: () => fetchSomeData() } export async function loader() { const queryClient = new QueryClient(); try { await queryClient.fetchQuery(someDataQuery); return json({ dehydrate: dehydrate(queryClient) }); } catch (e) { // decide whether to handle the error or continue to // render the page and retry the query in the client } } export default function MyRouteComponent() { const { dehydratedState } = useLoaderData<typeof loader>(); const { data } = useQuery(someDataQuery); return ( <HydrationBoundary state={dehydratedState}> <SomeComponent data={data} /> </HydrationBoundary /> ); }
I was initially skeptical about Tailwind, having never used it before, and because I didn’t quite understand the hype (it seemed to me at first just like syntactic sugar over CSS). However, I decided to give it a try because of its strong recommendations and popularity within the community, and I’m really glad I did. Tailwind’s utility-first approach made it incredibly easy to build a consistent and robust design system right from the start, which, looking back, was a total game changer.
It also pairs perfectly with shadcn, which we used, and together they allowed me to deliver quickly while keeping everything modular and easy to modify later on - a crucial advantage in a startup environment.
I also really like how easy it is to customize tailwind's theme to your needs - for example, overriding tailwind's default scheme:
First, define your colors as variable's under tailwind's main .css file:
@tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { /* define the primitive design system tokens */ --colors-blue-100: hsl(188 76% 90%); --colors-blue-200: hsl(187 63% 82%); --colors-blue-25: hsl(185 100% 98%); --colors-blue-300: hsl(190 52% 74%); --colors-blue-400: hsl(190 52% 61%); --colors-blue-50: hsl(188 92% 95%); --colors-blue-500: hsl(190 74% 39%); --colors-blue-600: hsl(191 77% 34%); --colors-blue-700: hsl(190 51% 35%); --colors-blue-800: hsl(191 52% 29%); --colors-blue-900: hsl(190 51% 23%); --colors-blue-950: hsl(190 52% 17%); --colors-gray-100: hsl(0 0 90%); --colors-gray-200: hsl(0 0 85%); --colors-gray-25: hsl(0 0 98%); --colors-gray-300: hsl(0 0 73%); --colors-gray-400: hsl(0 1% 62%); --colors-gray-50: hsl(0 0 94%); --colors-gray-500: hsl(0 0% 53%); --colors-gray-600: hsl(0 0 44%); --colors-gray-700: hsl(0 0 36%); --colors-gray-800: hsl(0 2% 28%); --colors-gray-900: hsl(0 0 20%); --colors-gray-950: hsl(0 0 5%); --colors-red-100: hsl(4 93% 94%); --colors-red-200: hsl(3 96% 89%); --colors-red-25: hsl(12 100% 99%); --colors-red-300: hsl(4 96% 80%); --colors-red-400: hsl(4 92% 69%); --colors-red-50: hsl(5 86% 97%); --colors-red-500: hsl(4 88% 61%); --colors-red-600: hsl(4 74% 49%); --colors-red-700: hsl(4 76% 40%); --colors-red-800: hsl(4 72% 33%); --colors-red-900: hsl(8 65% 29%); --colors-red-950: hsl(8 75% 19%); /* ... */ /* define the semantic design system tokens */ --primary-light: var(--colors-blue-200); --primary: var(--colors-blue-600); --primary-dark: var(--colors-blue-800); --primary-hover: var(--colors-blue-50); --text-default-primary: var(--colors-gray-700); --text-default-secondary: var(--colors-gray-800); --text-default-tertiary: var(--colors-gray-900); --text-default-disabled: var(--colors-gray-300); --text-default-read-only: var(--colors-gray-400); --disabled: var(--colors-gray-300); --tertiary: var(--colors-gray-50); /* ... */ } }
Then, extend Tailwind's default theme via the tailwind config file:
import { type Config } from 'tailwindcss'; const ColorTokens = { BLUE: 'blue', GRAY: 'gray', RED: 'red', } as const; const generateColorScale = (colorName: string) => { const scales = [25, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]; return scales.reduce( (acc, scale) => { acc[scale] = `var(--colors-${colorName}-${scale})`; return acc; }, {} as Record<string, string>, ); }; export const customColors = Object.values(ColorTokens).reduce((acc, color) => { return { ...acc, [color]: generateColorScale(color), }; }, {}); const config = { // ... additional config theme: { extend: { colors: customColors }, }, } satisfies Config; export default config;
This is just the tip of the iceberg - you can go on to define custom spacing, text sizing and much more!
Previously using Cypress, I was inclined to choose it, but I kept hearing hype around Playwright and figured I'll research it extensively before making a decision. After comparing Playwright with Cypress, it was clear Playwright is the right choice to make - the fact it comes with parallel execution out of the box, the broader browser support, running times and debugging capabilities - all made Playwright the obvious choice.
And, while this is very subjective, I like Playwright's syntax much better. I find it similar to React Testing Library's syntax, which I like, and I tend to think the tests are a lot more readable, with the asynchronous aspect of the tests being very straight forward, unlike the syntax of Cypress that can cause tests to feel bloated by .then() statements and subsequent indentations.
I think my favorite feature of Playwright is their implementation of Test Fixtures. They provide a clean way to initialize and reuse resources like page objects, making tests more modular and maintainable. Make sure to check out the above link to learn more about it!
First off, let me clarify — @tanstack/react-table is a fantastic tool, which is why I was inclined to choose it in the first place, but it wasn’t the best fit for my particular use case. The very features that make it great, like its small bundle size and customizable API, ended up being less relevant to our needs than I originally thought. Despite having full control of the rendering of the Table, I was having some issues aligning its scrolling behavior to our desired outcome (why is it still not possible in 2024 to have a