c-h-w-h/cds

Refactor: Yarn Berry 마이그레이션

prayinforrain opened this issue · 7 comments

♻️ 리팩토링 사항

  • ��Yarn 버전을 berry(2) 이상으로 업그레이드해요
  • nodeLinker를 PnP로 사용해요

📖 참고 사항

4월 30일 미팅에 있던 건데.. 일단 이슈로만 써놓을래요
작업 시점은 잘 모르겠어요 다음 버전 준비 전에 쇽 해 놓는 게 좋을 것 같은데 스토리북7 마이그레이션이랑 겹치면 또 피곤할 것 같아요
그렇게 어려운 작업은 아닌데 혹시 페어 하실 분 계시면 환영해요 ^^..

image

아마 별도 이슈를 열어야 할 수도 있는데.. 아직 상황 파악이 제대로 안됐어요
설명을 위해 지금 버전의 Container.stories.tsx를 예로 들게요
이 파일의 임포트는 지금 이 순서에요

import { COLOR } from '@constants/color';
import { css } from '@emotion/react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { ReactNode } from 'react';

import Container from '.';

그런데 yarn berry로 올리고 나니 린트 에러가 나기 시작했어요. 얘가 지금 주장하는 순서는 아래와 같아요:

import { COLOR } from '@constants/color';

import Container from '.';

import { css } from '@emotion/react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { ReactNode } from 'react';

우리 import/order 린트 룰 설정은 아래와 같아요.

   "import/order": [
      "error",
      {
        "groups": ["builtin", "external", ["parent", "sibling"], "index"],
        "pathGroups": [
          {
            "pattern": "angular", // 그리고 여기 "react"가 되어야 하는 게 아닌지 하는 생각이 들어요
            "group": "external",
            "position": "before"
          }
        ],
        "alphabetize": {
          "order": "asc",
          "caseInsensitive": true
        },
        "newlines-between": "always"
      }
    ],

이 링크를 따라서 풀어서 설명하면 다음 순서에요.

  • builtin - node 빌트인 패키지
  • external - 외부 패키지 모듈(yarn add로 설치된 것들)
  • [parent, sibling] - 상대 경로로 import해 오는 내부(internal) 모듈; 상위 폴더에서 가져오는 것(parent)과 같은 폴더에 있는 모듈(sibling)을 같은 우선순위로 둬요
  • index - 현재 디렉토리의 index 파일 모듈

제가 이해한 게 맞다면 기존 버전과 베리의 린트가 주장하는 순서는 둘 다 틀렸어요. 이유는 두 가지인데

  • 기존 버전은 @constants/로 임포트한 internal module과 @emotion @storybook으로 임포트한 external module이 같은 그룹으로 되어 있어요.("newlines-between": "always"에 의한 줄바꿈이 되어 있지 않아요)
  • 새 버전은 가장 뒤에 와야 할 index 모듈의 뒤에 external이 오고 있어요.

여기까지가 슬랙에서 @se030 님과 대화한 내용이고 저는 배가 고파서 집에 가야하니까 나머지는 나중에 정리하려구요!
정 생각하기가 귀찮다면 eslint-plugin-simple-import-sort 라는 플러그인을 시도해봐도 좋을 것 같아요. 넘블에서 다른 팀원 분이 알려주셨는데 리얼굿이었어요

import/order가 사용하는 다른 플러그인의 설정을 보면 yarn pnp를 사용할 경우 .eslintrc의 settings에 아래 내용을 넣어야 한대요.

"import/external-module-folders": [".yarn"]

넣었더니 기존처럼 internal-external 구분 없이 모두가 external로 들어가게 되었어요.

정답을 찾았어요.

우선 internal, external 모듈의 정의는 우리가 아는 것과 같아요. 다만 지금 우리 룰에는 internal의 순서가 따로 지정되어 있지 않아요.
parent와 sibling이 있는데 왜 이게 문제냐면, parent와 sibling은 상대 경로에 의해 판단되는 모듈이니까 우리가 사용하는 alias에 의한 internal 모듈은 여기에 포함되지 않기 때문이에요.(플러그인이 tsconfig의 alias 정의를 불러오지는 않는 것으로 이해했어요). 따라서 internal을 external과 parent-sibling 그룹 사이에 추가해요.

        "groups": [
          "builtin",
          "external",
          "internal",
          ["parent", "sibling"],
          "index"
        ],

이제 emotion, react와 우리가 사용하는 internal 모듈이 둘 다 external로 인식되는 이유에요. 이건 여기에 정의되어 있는데,

  1. import/external-module-folders 옵션에 지정되어 있거나
  2. node_modules에 들어 있거나
  3. 현재 위치를 기준으로 상대 경로를 구해서 ..으로 시작하거나

이 세 가지 경우를 모두 external로 인식하고 있어요. 따라서 alias들을 resolve하면 무조건 상위 폴더로 나가기 때문에 external이 돼요.
(참고로 import문의 경로 자체가 ..로 시작하는 경우는 앞에서 parent로 걸러요)

아무튼 이 모든 것들이 external이 되니까 우리가 선언한 alias들을 예외적으로 pathGroups 옵션에 등록해서 internal로 처리해 주어야 해요. patterns는 minimatch라는 라이브러리를 통해 정규식이랑 비슷하지만 다른 glob 표기법으로 검사되는데.. 이 부분은 챗쌤한테 물어물어 해결한거라 생략할게요.

          {
            "pattern": "@?(components-common|components-layout|components|constants|styles|util-types|utils)/**",
            "group": "internal"
          },
          {
            "pattern": "@?(components-common|components-layout|components|constants|styles|util-types|utils)",
            "group": "internal"
          },

이제 alias로 선언된 대부분의 import가 internal로 처리되었어요.
왜 두 개의 규칙이 있냐면, alias는 하위 디렉토리 없이 사용되는 경우가 있기 때문이에요. Drawers 컴포넌트에 용례가 있어요. 따라서 하위 디렉토리 없이 alias만 있는 경우를 처리해야 해요. 아쉽게도 /**?를 붙여 사용할 수는 없는 것 같아서 pathGroups에 규칙을 추가했어요.
�좀 더 우아한 방법을 찾고 싶었는데.. 그러려면 .eslintrc를 js 파일로 바꿔야 하는 고통이 있어서 그렇게 하지 않았어요. 개인적으로는 지금 형태가 더 설정하기 익숙하거든요..

          {
            "pattern": "src/**",
            "group": "internal"
          }

마지막으로 useSafeContext를 internal로 취급하기 위해 위 규칙을 추가하면 우리의 진짜최초리얼처음정했던오리지널 순서가 적용돼요. 근데 이거 왜 alias 지정 안했죠?

아무튼 바로 윗 코멘트 + 여기까지가 제가 해결한 과정인데 이걸 이 브랜치에 섞어서 올리는게 맞는지 잘 모르겠어요.

se030 commented

고생하셨어요 ~ 아직 읽어보지는 못했는데 작업은 다른 이슈로 나누는게 좋을 것 같아요?

작업 근황(개인 메모)

storybook 의존성 명시 041d327

yarn berry로 전환 후 스토리북이 실행되지 않았어요. 이유는 아래 링크에 나와 있는데, 요약하면 yarn pnp가 기본적으로 peer dependency를 엄격하게 관리하기 때문이에요. 잘못 요약했어요 그냥 의존성 해결 방식이 달라서 그래요. 발표 준비할게요..

사실 pnpMode: loose 하면 대부분 해결이 되는데, 프로젝트 규모가 커지면(의존성 트리가 복잡해 지면) 예기치 못한 문제를 일으킬 수 있다고 해서, 요구하는 종속성을 일일이 설치했어요. @storybook/어쩌고 패키지들과 @types/어쩌고들이 그것이에요.

react-icons 재설치 f2ab989

기본에는 @react-icons/all-files를 설치해서 사용했어요. 제가 파악하기로 이는 react-icons 패키지와 동일한데, pnp모드의 종속성 관리 문제 탓인지 react-icons/md의 형태로 import 하는 파일들이 resolve되지 않아서 재설치했어요.

import/order 63190eb

위 댓글의 내용은 별도 이슈로 분리했어요. 이유도 거기 적혀있어요.

이 브랜치에서 해 줄 변경은 그저 외부 패키지를 .yarn 디렉토리에서 사용한다고 명시하는 것 뿐이에요.

prettier 설치 667f6b0

pnp 모드에서는 workspace 내의 typescript와 eslint, prettier 등을 사용해요. yarn dlx @yarnpkg/sdks vscode 명령어로 vscode에서 사용할 플러그인 파일들을 만들 수 있어요.
그런데 cds에는 prettier가 설치되어 있지 않았는데, 이게 husky에서 문제를 일으켰어요. 그래서 prettier를 개발 종속성으로 설치하고 sdks에 포함했어요.

남은 문제

build가 안돼요..
image
모르겠어요 이거 왜이래요..?

작업 근황(개인 메모)

gitignore 수정 2ca3569 5d42cbb

제가 이전에 했던 프로젝트의 .gitignore를 가져와서 적용했었는데... .pnp.cjs.pnp.loader.mjs 파일이 ignore되지 않고 있었어요. 저는 바보에요..
왜 무시하냐? 이 파일들은 yarn.lock과 비슷하게 pnp mode에서 의존성을 resolve하는 규칙을 담고 있어요. 다만 각 패키지의 세부 버전은 yarn.lock으로 관리되고 있고, 두 개의 환경에서 테스트한 결과 같은 yarn.lock으로부터 같은 결과를 생성하고, 당장 zero-install 기능을 사용하지 않기 때문이에요. 무엇보다 의존성 관련 conflict가 발생할 경우 resolve하기 너무 버거워요 ㅋㅋ.. 이 부분은 어떻게 쉽게 resolve 가능한지 알아봐야 해요
Yarn berry의 FAQ 페이지에 자세한 설명이 있어요.

왜 제로인스톨 안쓰냐면.. 제 환경 기준으로 pnp를 사용하는 것 자체로 이미 node_modules 링커보다 3배 이상 빠르기 때문이에요. 필요하다면 PR 머지 직전에 추가할 것 같은데, 라이브서버에 배포하는 것이 아닌 cds 특성상 굳이 필요한가 싶은 생각이 들어요.(제 생각)

왜 나만 빌드가 안 돼

공채 시즌 전에 제 환경에서만 스토리북 실행이나 빌드가 안 된다는 이야기를 했어요.
정~말정말 복잡한 과정이 있었는데.. 결론적으로는 그냥 고쳐졌어요.
package.jsonimports 필드가 명시되었을 때 모듈 임포트 경로가 제대로 resolve가 되지 않는 문제였는데..
아마 rollup/rollup#4496 이 이슈로 추정되는데.. 아무튼 vite 버전을 올렸더니 -> rollup 버전도 올라갔고 -> 이유야 어찌 됐든 지금은 돼요. 굿 🎉🎉

pnp에서 build가 안 돼

dev까진 정상적으로 돌아갔는데 build가 안되는 문제가 있었어요.
오류 메시지를 보면 react, @emotion/styled를 포함한 모든 external module이 resolve되지 않는 문제였는데,
build 실행할 때 external 모듈을 resolve할 경로를 찾지 못하는 문제였어요. @yarnpkg/pnpify를 사용하면 이런 경로 문제를 (대부분) 해결할 수 있어요. build 명령어를 수정했씁니다,,

그런데 세 개의 에러가 남았어요.

[vite:dts] Start generate declaration files...
src/components/Button/useButtonStyle.tsx:4:7 - error TS2742: The inferred type of 'useButtonStyle' cannot be named without a reference to '../../../.yarn/cache/csstype-npm-3.1.2-cead7d99b2-e1a52e6c25.zip/node_modules/csstype'. This is likely not portable. A type annotation is necessary.

4 const useButtonStyle = (variant: ButtonVariant) => {
        ~~~~~~~~~~~~~~
src/components/Slider/useSlider.ts:20:7 - error TS2742: The inferred type of 'useSlider' cannot be named without a reference to '../../../.yarn/cache/csstype-npm-3.1.2-cead7d99b2-e1a52e6c25.zip/node_modules/csstype'. This is likely not portable. A type annotation is necessary.

20 const useSlider = ({
         ~~~~~~~~~
src/styles/flex-box.ts:12:14 - error TS2742: The inferred type of 'flexboxStyle' cannot be named without a reference to '../../.yarn/cache/csstype-npm-3.1.2-cead7d99b2-e1a52e6c25.zip/node_modules/csstype'. This is likely not portable. A type annotation is necessary.

12 export const flexboxStyle = ({
                ~~~~~~~~~~~~

이게 뭔지는 지금부터 알아갈 예정이에요 ^^.. 용의자 리스트는 아래와 같아요

전혀 다른 문제지만 아래 코멘트에서 힌트를 얻었어요.
세영님이 확인해 주신대로 리액트의 CSSProperties 타입은 csstype 패키지의 것을 그대로 전달해 주는 역할이에요.

좀 더 자세한 원인을 알고 싶지만.. 막연히 yarn berry에서의 resolve하는 방식의 차이가 있어 이런 문제가 발생한다고 생각할 수 밖에 없겠어요. 그냥 얀베리가 잘못했겠지~ 하는 수준..
그래서 5abce46 에서 CSSProperties 타입을 CDS 내에 직접 정의하고, 코드 일관성을 위해 모든 CSSProperties를 이 쪽으로 옮겼어요.

useSlider의 경우에는 좀 특이한 케이스에요. CSSProperties를 변경해 주었음에도 에러가 사라지지 않았어요. CSSProperties를 사용하는 복잡한 타입에 대해서 TS2742 오류가 발생한다 라는 힌트를 여기서 얻었는데, useSlider가 반환하는 객체는

{
  // ...
  getStyles: () => {
    rootStyle: Partial<CSSProperties>;
    trackStyle: Partial<CSSProperties>;
    filledStyle: Partial<CSSProperties>;
    thumbStyle: Partial<CSSProperties>;
  },
  // ...
}

대충 이런 식이에요.
getStyles가 반환하는 객체의 타입은 4개의 Partial<CSSProperties>로 축약이 가능하지만 실제로 ts가 자동으로 추론했을 때에는 훨씬 복잡한 오브젝트 리터럴이었을 거에요. 그래서 getStyles만 별도의 타입을 선언해 주어 해결했어요.

너무 맥락이 다르고 큰 범주의 변경이어서 별도 브랜치에서 작업하고 싶었는데, 결국 CSSProperties -> yarn berry migration -> main 이 방향으로 머지하다보면 스쿼시 머지 때문에 의미가 없을 것 같아서 바로 올렸어요. useSlider와 flexbox, button 이 세 개를 작업한 세영님과 현빈님한테 PR때 한 번 더 확인받을게요.
dev와 build 모두 제대로 동작하는데 배포해서 정상적으로 사용이 가능한지도 확인해 봐야 할 것 같아요. 더 이상 괜찮겠지 하는 믿음이 없어요 ㅋㅋ...