2023. 8. 13. 16:58ㆍ공부내용 공유하기
회사의 스토리북 유지보수 업무를 하면서 Footer와 같은 공통 컴포넌트를 스토리북에 넣어줘야 하는 일이 있었다.
큰 문제없이 작업을 완료하고 원격 저장소에 push를 하니, 로컬에서 빌드되던 스토리북의 배포가 실패하고 있었다.
원인을 살펴보니, Footer 내부에 graphql codgen으로 생성된 typescript 파일에 대한 의존성이 있어서 codegen을 하지 않는 배포환경에서의 스토리북 빌드가 실패하던 것이었다.
정적인 데이터를 위주로 회사의 기본적인 정보를 보여주는 Footer였기 때문에 어째서 서버 타입의 의존성이 있는지 이해하지 못했으나, 자세히 살펴보니 서버 데이터를 기반으로 codgen을 해서 사용하는 일부 enum 값에 대해서 상수처럼 사용하고 있었다.
예를 들어 대표적으로는 다국어 서비스를 하는 우리 회사에서 지원하는 언어와 화폐에 대한 값들이 그에 해당한다. 사실상 상수로 사용되지만 데이터의 출처가 graphql enum인 경우이다.
import { CurrencyType, LanguageType } from 'graphql/types';
export enum LanguageType {
Chinese = 'CHINESE',
English = 'ENGLISH',
Hongkong = 'HONGKONG',
Japanese = 'JAPANESE',
Korean = 'KOREAN',
Taiwan = 'TAIWAN',
Thai = 'THAI',
Vietnamese = 'VIETNAMESE'
}
이러한 상수 enum에 대해 의존성을 가지고 있는 코드들이 Footer 내부에도 존재했고, 이 때문에 빌드가 실패했던 것이다.
배포 환경에서 스토리북 빌드 전에 codegen을 하는 방법도 있었지만, 디자인 시스템을 보여주려는 원래 목적과 다르다고 생각했고, 이러한 상수 값들이 아닌 서버 의존성이 존재한다면 해당 컴포넌트를 스토리북에 등록할 상황 자체가 생기지 않는 게 맞다고 판단했다.
결론적으로 몇 가지 대표적으로 사용되는 상수들에 대해서만 모킹을 하기로 결정했고, 모킹 자체는 스토리북 웹팩 설정에서 간단하게 해줄 수 있었다.
import type { StorybookConfig } from '@storybook/core-webpack';
const config: StorybookConfig = {
// ...
webpackFinal: (config) => {
if (config.resolve?.alias) {
config.resolve.alias['graphql/types'] = require.resolve('./__mocks__/graphqlTypes.ts');
}
return config;
},
};
export default config;
모킹 파일은 대략 아래와 같이 모킹이 필요한 대표적인 enum 값들만 기재해두었다.
// .storybook/__mocks__/graphqlTypes.ts
enum LanguageType {
Chinese = 'CHINESE',
English = 'ENGLISH',
Hongkong = 'HONGKONG',
Japanese = 'JAPANESE',
Korean = 'KOREAN',
Taiwan = 'TAIWAN',
Thai = 'THAI',
Vietnamese = 'VIETNAMESE',
}
LanguageType enum을 export 하는 순간 프로젝트의 모든 경로에서 모킹 파일에 있는 LanguageType을 참조할 수 있기 때문에 한 객체에 모아서 export를 해준다.
module.exports = {
LanguageType,
CurrencyType,
};
javascript에서는 export 예약어를 사용할 경우 모듈을 내보낼 범위에 대해서 명시적으로 나타낼 수 없기 때문에 아래와 같은 문제가 흔하게 발생한다.
모킹 객체를 만들어서 모킹을 했지만, 직접 모킹하지 않은 프로퍼티 접근을 감지하고 에러 메시지를 출력해주기 위해 Proxy를 사용하기로 결정했다.
방법은 다음과 같다.
- proxy 객체를 만들어서 모킹하지 않은(하지 않을) prop 접근을 감지해서 에러를 발생시킨다.
- proxy 객체를 module.exports로 내보낸다.
const mockTypes = {
LanguageType,
CurrencyType,
CountryType,
CountryCallingCodeType,
};
const mockingProxy = new Proxy(mockTypes, {
get: (target, prop) => {
if (prop === '__esModule') {
return;
}
if (Reflect.has(mockTypes, prop)) {
return Reflect.get(mockTypes, prop);
}
throw Error(
`모킹되지 않은 enum값이 사용되었습니다. 서버 의존성이 있는 컴포넌트를 스토리북에서 사용하는걸 자제해주세요. [${prop.toString()}]`,
);
},
});
module.exports = mockingProxy;
천천히 살펴보자. 먼저 mockTypes 객체의 프로퍼티에 접근하는 요소들을 다루기 위해 get Handler를 커스터미아징 했다.
'__esMoudle' 프로퍼티는 bebel의 트랜스파일 과정에서 해당 파일의 모듈 시스템에 따라 선택적으로 추가되는 값이다. 번들링 과정에서 babel이 ESM과 CommonJS 모듈 모두 동일한 인터페이스로 사용할 수 있게 하기 위해 ESM 기반의 모듈에 '__esModule'이라는 프로퍼티를 true로 할당하여 일종의 flag로 사용한다.
'__esModule'은 다음과 같이 모듈을 불러오는 과정에서 같은 식별 함수를 통해 동일한 인터페이스로 처리될 수 있게 된다.
function esModuleInterop(module) {
return module.__esModule ? module : {default: module};
}
트랜스 파일 과정에서의 CommonJS, ESM 처리는 아래 글에서 자세히 볼 수 있다.
https://ui.toast.com/weekly-pick/ko_20190418
따라서 babel을 통해 트랜스파일링 된 코드에서 '__esModule' 프로퍼티에 대한 접근은 필연적으로 발생할 수밖에 없으니 해당 프로퍼티의 접근에 대해서는 에러를 던지지 않도록 처리를 해 둔다.
최종적으로 이러한 처리를 통해 만들어진 Proxy 객체를 CommonJS 방식으로 내보내고 webpack 설정에서 require를 통해 가져오면 된다.
참고