Remix.js V2 살펴보기 | Map Service 구축하기 - 1

    반응형

     

     

    전회사에서 사내 어드민 프로젝트로 Remix.js + React-query 환경에 작업을 했었습니다

    그때 처음 알게 된 Remix.js는 좀 독특하다고 느껴졌었는데요

    이번에 토이 프로젝트로 Remix.js로 풀스택 개발을 해보자고 결심했습니다

    근데 웬걸... 버전 2가 나왔더라고요?

    또한 Remix.js를 온전히 써보려고 하니 생각보다 제가 Remix.js 생태계 자체를 잘 이해한 건 아니구나 라는 생각이 들었습니다

    이번 글에서는 Remix.js를 써보면서 신기하다고 생각했던 점을 쭈욱 써볼까 합니다

     

     


     

     

     

    파일 시스템 규칙


    Remix.js는 React Router 위에 구축이 되었다고 합니다

    그래서 React Router 가 기본으로 내장되어있고, 우린 Nested Routes를 구성할 수 있습니다

    V1에서는 폴더를 생성해서 router를 묶고, 구성을 했었습니다

    // v1
    routes
    |
    |- auth
    |   ├── index.tsx
    |   ├── login.tsx
    |   └── signup.tsx
    |
    ├── auth.tsx
    └── index.tsx
    파일 URL 설명
    index.tsx / 메인 root
    auth.tsx 없음 /auth의 레이아웃이 되는 파일
    routes/auth/index.tsx /auth /auth의 메인
    routes/auth/login.tsx /auth/login 로그인 페이지
    routes/auth/signup.tsx /auth/signup 회원가입 페이지

     

    V2에서는 폴더로 구성하는 것이 아닌 플랫 경로 파일 시스템을 따르라고 권합니다

    플랫 경로는 파일 이름 자체가 URL 경로가 되는 것입니다

    // v2
    routes
    |
    ├── _auth.tsx
    ├── _auth.login.tsx
    ├── _auth.signup.tsx
    └── _index.tsx
    파일 URL 설명
    index.tsx / 메인 root
    _auth.tsx 없음 /auth의 레이아웃이 되는 파일
    _auth.login.tsx /login 로그인 페이지
    _auth.signup.tsx /signup 회원가입 페이지

    위와 같이 파일들의 폴더 뎁스가 사라지고 1뎁스에 쭈욱 나열되는 형태가 되죠

    언더바 말고도 다양한 컨벤션이 있습니다

    파일 컨벤션 URL 설명
    about.tsx .  /about 이 밑에 하위 경로가 생기면 하위 경로들의 레이아웃이 되는 상위 경로
    about.me.tsx . /about/me 일반 경로
    about._index,tsx _ (선행 밑줄) 없음 이 경우는 잘못된 중첩입니다
    _index.tsx는 고유명사 같은 개념이기에 쓸 수 없습니다
    about._layout.tsx _ (선행 밑줄) /about 해당 파일은 about 하위 경로들의 레이아웃 파일이 됩니다
    about._layout.me.tsx _ (선행 밑줄) /about/me about의 하위 경로
    _layout 안에 me 파일이 랜더링
    about_.layout.tsx _ (후행 밑줄) /about/layout 루트가 되는 /about 경로 없음
    about_.layout.me.tsx _ (후행 밑줄) /about/layout/me 일반 경로
    docs.$.tsx $ (베어)   /docs 스플랫 루트라고 불리는데 사실 이게 어떤 형태로 쓰일지 잘 모르겠습니다
    특징으로는 해당 경로에 뒤에 무엇이 붙던 (Ex. "/docs/test/", "/docs/test/lalal")
    docs.$.tsx 파일을 랜더링 해준다는 겁니다
    users.$userId.tsx $ (선두) /users/34 URL 매개변수
    [sitemap2.xml].tsx [] /sitemap2.xml  
    /products.($lang)._index.tsx () /products/en 선택적 세그먼트로 ($lang)이 매개변수처럼 할당되는 구조입니다

    더 자세한 경우들은 공식문서에서 확인 가능합니다

     

    개인적으로 이렇게 써보니 느꼈던 부분과 의문점들이 있었습니다

    1. 불필요하게 깊어지기만 하는 폴더 뎁스가 없어짐
      1. 누군가에겐 파일양이 많아질수록 가독성이 좋지 않다는 취향차이가 생길 수도 있을 것 같음 (점 구별하는 거 솔직히 한눈에 잘 보이는 건 아니니깐...!)
      2. 규모가 큰 프로젝트일 경우 파일이름이 점점 더 길어질텐데 과연 이런 상황 또한 좋을까?
    2. 불필요하게 붙어야했던 일반 경로들을 줄일 수 있음
    3. 잘 구성하면 레이아웃 컴포넌트를 따로 구성하지 않고 어느 경로에서나 공통된 레이아웃을 만들 수 있음
    4. 분기점이 되는 기능 컴포넌트를 따로 안 만들 수 있을 것 같음 (레이아웃 파일에서 처리한다던지?) 
      1. 경로 파일이 컨트롤러 역할을 할 수 있지 않을까?

     

    1번 같은 경우에는 개선사항으로 경로와 파일 규칙을 함께 쓸 수 있는 라이브러리를 제공하는 것 같습니다

    쨌든, 개인적으로 바뀐 컨벤션이 굉장히 마음에 듭니다

    본인이 파일 구조와 기능을 어떻게 쪼개냐에 따라 굳이 layouts, feature 안에 파일을 따로 안 만들고 해결할 수 있는 느낌이라 생각이 듭니다

     

     

     

    왜 Vite로 갈아탔을까?


    V1에서 Remix.js를 시작하면 remix.config.js란 파일이 생겼었는데,

    V2에서는 더이상 제공되지 않습니다

    그 이유는 이제 Vite를 쓰기 시작했기 때문입니다

    대체 왜? 일까 싶었습니다 자체 컴파일러로 구성하다가 왜 바꿨을까? 싶더라고요

    검색을 해보니 관련된 이슈를 발견했습니다

    HMR이 부족하다고 하는데, 사실 그랬던가? 싶었습니다

    HMR이란?
    앱을 종료하지 않고 갱신된 파일만을 교체하는 방식

    생각해 보니 이번 V2로 바뀌면서 기존에 파일이 변경되면 자동으로 페이지를 리로드 시켜주는 <LiveReload />도 사라졌습니다

    곰곰이 생각해 보니 제가 겪었던 문제가 HMR 부족 때문이었나? 싶었던 부분들이 있습니다

    • 첫 로드 시 tailwind CSS가 적용되지 않는 화면으로 보이다가 깜빡임 후 다시 리로드 되면서 CSS 적용됨 (이 경우는 개발환경에서만 발생)
    • 가끔 운영 환경에서도 CSS가 누락되는 경우가 발생 (이 경우 에러가 발생해서 여러 번의 재랜더링이 일어났다가 최종 랜더링된 화면에서 누락되는 경우가 발생 => 다시 수동 새로고침 하면 CSS 적용됨)
    • 가끔 개발 환경에서 리로드가 안되어 수동 새로고침 할 때도 있음

    그래서 왜 뒤늦게 vite로 갈아탔느냐? 이유는 간단했습니다

    Remix.js 개발은 2020년에 시작했고, Vite의 release 버전은 2021년에 출시되었기 때문이다

    자체 컴파일러를 만드는게 리믹스 팀의 궁극적인 목표가 아니었다고 하네요

    Remix.js의 가장 큰 특징이 따로 server를 구축하기 위한 라이브러리를 쓸 필요 없이,

    server용 코드를 작성해 DB에서 바로 데이터를 받아와 client에 뿌려줄 수 있다는 것인데요

    아마 빌드 단계에서 server 빌드와 client 빌드를 동시 진행 후 변화된 데이터를 client에 반환 후 자동 리로드 되는 지원이 부족했던 게 아닐까?라고 추측해 봅니다

    제가 빌드 관련해서 깊게 파보고 직접 한 땀 한 땀 구성해 봤으면 더 큰 차이점을 명확히 알겠는데,

    사실 현재로선 큰 차이점은 못 느끼고 있습니다

    더불어 Vite를 쓰기 시작하면서 기본적으로 PostCSS이 내장되어 있기 때문일까요?

    토이 프로젝트를 진행하면서 아직까지는 제가 겪었던 CSS 누락 문제는 겪고 있지 않습니다

     

     

     

    매우 빠른 Server code 작성


    전회사에서 Remix.js + React-query 환경에서 개발해 왔다고 했잖아요?

    서버는 python으로 구축된 서버에서 API 주소를 통해 받아왔습니다

    그래서 사실 리믹스를 제대로 써봤다고 보긴 힘들더라고요

    리믹스의 특징을 하나도 활용하지 않은 셈이었습니다

    요번에 토이 프로젝트로 Remix.js 하나만으로 개발을 해보면서 신기한 기능이다!라고 느꼈던걸 말해보려고 합니다

     

    .server 모듈

    서버에서만 실행할 코드들을 작성할 수 있습니다

    여기에 DB에 접근하는 코드를 쓰면 되는데,

    그래서 특징이 REST API가 아니란 겁니다
    (만약 API 주소를 설정할 수 있다면 댓글 부탁드립니다!)

    우리는 보통 API를 만들 때 아래와 같이 만들고 호출합니다

    // server code
    app.use("/user", () => { /*... */ })
    
    // client code
    axios.get("/user")

     

    그러나 리믹스 문서에서도 소개하듯 Remix app server를 쓰면 아래와 같이 씁니다

    // server code
    export async function createPost(data) { /*...*/ }
    
    // client code
    import { createPost } from "~/.server/post";
    
    export async function loader() {
      const result = await createPost();
      return json(result);
    }

    이렇게 쓰면 또 특징이 있습니다

    • loader, action을 통해 접근하는 게 아니라면 에러를 반환
    • cors 에러가 안 뜬다
    • 데이터 update 시 변경된 데이터를 바로 호출할 수 있다 (리로드 X, react-query의 특징이 이미 탑재된 느낌)

     

    그러나 저는 문제 상황을 마주하기도 했는데요...!

     

     

     

    Remix.js를 사용하면서 불편했던 점


    이건 사실.. 아직도 왜? 이러는가? 하는 의문도 있습니다 
    (이유를 아시는 분 계시다면 댓글 부탁드립니다)

    loader 

    onClick 했을 때만 데이터를 받아오고 싶을 때의 번거로움이 있습니다

    loader 같은 경우에는 해당 페이지에 접근하면 무조건 실행되는 함수입니다

    근데 저 같은 경우에는 클릭 이벤트가 발생했을 때만 해당 데이터를 받아오고 싶다거나,

    param에 query string이 있으면 실행되거나 하는 경우로 만들어 주고 싶었는데

    client에서 서버 코드를 직접 접근하는 것도 불가능하고, loader 함수를 조건에 맞게 제어할 수도 없고 component 함수 안에 작성된 매개변수를 전달할 수도 없어서 굉장히 애를 먹었습니다

    그렇게 제가 해결한 방식은 loader는 loader대로 실행은 되도록 하고,
    (loader 함수 자체의 실행은 막을 순 없고 안에서 if문을 써서 get 함수를 제어)

    useFetcher hook을 이용해 query string이 있으면 loader를 한번 더 실행해서 fetcher.data에 담긴 데이터를 이용하거나
    (웃긴 건 loader는 null을 반환합니다 이건 대체 왜...? 같은 함수 실행한 건데...)

    router를 쪼개서 해당 페이지를 레이아웃으로 감싸주거나 했어야 했습니다

    뭐가 되었든 특정 조건이 되었을 때만 실행되어야 할 코드가 무조건적으로 실행이 된다는 건데

    이게 성능적으로 좋은 건지 모르겠습니다

    물론 제가 잘 몰라서 이런 식으로 하는 거일 수도 있습니다 (...)

     

    action

    데이터를 post, put, delete 할 때 사용되는 함수인데 

    이게 어떤 상황에서는 action을 사용하고, 어떤 상황에서는 useFetcher를 쓰라고 합니다

    사용의 가장 큰 차이점은 URL이 변경되어야 하냐, 아니냐의 큰 차이 같습니다

    자세한 내용은 공식문서에서 확인 가능합니다

    그러나 공식문서에서 말하는 상황 외에 다른 상황이 있었는데,

    form 안에 작성된 input으로 받는 form data 외에 location state로 받은 데이터도 포함시켜야 할 때입니다

    action 함수 같은 경우에도 컴포넌트 안에서 매개변수처럼 action에 직접 값 전달이 불가능합니다

    그래서 무조건 input에 담아야 하다 보니 아래와 같은 코드를 써줘야 합니다

      {location.state && (
        <>
          <input
            name="cafeId"
            value={location.state.cafeId}
            readOnly
            hidden
          />
          <input
            name="name"
            value={location.state.name}
            readOnly
            hidden
          />
          <input
            name="reviewId"
            value={location.state.reviewId}
            readOnly
            hidden
          />
        </>
      )}

    hidden input을 생성하는 것이죠...

    사실 개인적으로... 좀 짜치는 코드라고 생각합니다

    그래서 이렇게 하고 싶지 않으면 fetcher.submit을 이용하면 될 것 같긴 합니다

    쨌건 여러모로... 같은 기능을 하는 함수가 파편화된 느낌...? 굳이??? 이런 느낌???

    결론은 action과 loader를 직접 제어하고 바로 접근하는 게 불가능하다 보니 우회하는듯한 코드가 많이 생기는 느낌입니다

    (혹시 다른 방법 또는 잘못 알고 있는 점 있다면 알려주세요!)

     

     

     

    마무리


    아직은 Remix.js와 친해지는 단계에 있어서

    제가 잘못 알고 있고, 잘못 해석한 경우들도 있을 것 같습니다

    피드백 적극 환영합니다!!!!

    앞으로 리믹스를 활용한 토이 플젝으로 찾아뵙겠습니다😊

    반응형

    댓글