[babel] babel-loader로 emotion 스타일링 적용 오류 해결

Gyuhan Park·2024년 7월 19일
1

Trouble Shooting

목록 보기
6/10

📘 문제 상황

개발환경에서는 ts-loader를 사용하여 빌드하였는데, 배포 관련한 작업이 필요해 프로덕션 환경에서 babel-loader를 사용하여 빌드했더니 에러가 발생하였다. React is not defined 오류는 React 17 미만 버전에서 jsx를 해석할 때 React.createElement로 트랜스파일되기 때문에 발생하는 오류로 판단하였다. 하지만 runtime: “automatic” 을 적용하면 새로운 jsx 변환 방식을 사용하기 때문에 React import가 필요없다고 알고 있어서 이해가 되지 않았다.

// .babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "> 2%, not dead",
        "useBuiltIns": "usage",
        "corejs": "3.37",
        "shippedProposals": true
      }
    ],
    "@babel/preset-typescript",
    ["@babel/preset-react", { "runtime": "automatic" }],
    "@emotion/babel-preset-css-prop"
  ],
  "plugins": ["@emotion/babel-plugin"]
}

📘 babel-cli 테스트

기존 바벨 테스트했을 때와 비교해보면 추가된 것은 css prop 뿐이다. 따라서 @emotion/babel-preset-css-prop 설정을 자세히 알아보았다. webpack의 문제일 수 있으므로 babel-cli를 이용하여 트랜스파일을 진행해보기로 하였다. 그래서 css prop을 사용하는 간단한 tsx 파일을 만들고 babel 명령어로 tsx 파일을 js로 만들어보았다.

$ npx babel ./src/Test.tsx --out-file ./dist/Test.js

🚨 @emotion/babel-preset-css-prop 가 맨 뒤에 위치할 때

var _react = require("@emotion/react"); : emotion에 정의된 React 모듈을 가져온다.
return (0, _react.jsx)("div", {}) : emotion React 모듈의 jsx 함수를 실행한다.
따라서 이 방식은 React를 import 하지 않으면 오류가 발생한다. "runtime": "automatic" 을 추가해서 문제가 없어야하는데 오류가 나는 것을 보면 automatic이 제대로 동작하지 않는 것을 확인할 수 있다.

Test.tsx

const Test = () => {
  return (
    <div
      css={{
        color: 'hotpink',
        '&:hover': {
          color: 'darkorchid',
        },
      }}
    >
      Test
    </div>
  );
};

export default Test;

.babelrc.json

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "> 2%, not dead",
        "useBuiltIns": "usage",
        "corejs": "3.37",
        "shippedProposals": true
      }
    ],
    "@babel/preset-typescript",
    ["@babel/preset-react", { "runtime": "automatic" }],
    "@emotion/babel-preset-css-prop"
  ]
}

dist/Test.js

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
var _react = require("@emotion/react");
function _EMOTION_STRINGIFIED_CSS_ERROR__() { return "You have tried to stringify object returned from `css` function. It isn't supposed to be used directly (e.g. as value of the `className` prop), but rather handed to emotion so it can handle it (e.g. as value of `css` prop)."; }
var _ref = process.env.NODE_ENV === "production" ? {
  name: "1msfjgq",
  styles: "color:hotpink;&:hover{color:darkorchid;}"
} : {
  name: "xkig9r-Test",
  styles: "color:hotpink;&:hover{color:darkorchid;};label:Test;",
  map: "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9UZXN0LnRzeCJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFHTSIsImZpbGUiOiIuLi9zcmMvVGVzdC50c3giLCJzb3VyY2VzQ29udGVudCI6WyJjb25zdCBUZXN0ID0gKCkgPT4ge1xuICByZXR1cm4gKFxuICAgIDxkaXZcbiAgICAgIGNzcz17e1xuICAgICAgICBjb2xvcjogJ2hvdHBpbmsnLFxuICAgICAgICAnJjpob3Zlcic6IHtcbiAgICAgICAgICBjb2xvcjogJ2RhcmtvcmNoaWQnLFxuICAgICAgICB9LFxuICAgICAgfX1cbiAgICA+XG4gICAgICBUZXN0XG4gICAgPC9kaXY+XG4gICk7XG59O1xuXG5leHBvcnQgZGVmYXVsdCBUZXN0O1xuIl19 */",
  toString: _EMOTION_STRINGIFIED_CSS_ERROR__
};
const Test = () => {
  return (0, _react.jsx)("div", {
    css: _ref
  }, "Test");
};
var _default = exports.default = Test;

✅ @emotion/babel-preset-css-prop 가 react, typescript 앞에 위치할 때 (해결)

@emotion/babel-preset-css-prop 은 preset/react와 preset/typescript 보다 앞에 위치해야 한다!!!
"importSource": "@emotion/react" 를 적용해야 react가 아닌 emotion 에서 jsx 함수를 가져온다.
이렇게 하면 @emotion/react/jsx-runtime 에서 _jsxRuntime 함수를 가져와 실행하는 것을 확인할 수 있다.

.babelrc.json

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "> 2%, not dead",
        "useBuiltIns": "usage",
        "corejs": "3.37",
        "shippedProposals": true
      }
    ],
    "@emotion/babel-preset-css-prop",
    "@babel/preset-typescript",
    ["@babel/preset-react", { "runtime": "automatic", "importSource": "@emotion/react" }]
  ]
}

dist/Test.js

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
var _jsxRuntime = require("@emotion/react/jsx-runtime");
var _react = require("@emotion/react");
...
const Test = () => {
  return (0, _jsxRuntime.jsx)("div", {
    css: _ref,
    children: "Test"
  });
};
var _default = exports.default = Test;

dist/Test.js

아래 실행 결과는 importSource를 명시하지 않았을 때의 트랜스파일 결과다. "runtime": "automatic" 이 적용되어 jsxRuntime 함수를 실행하지만 react에서 가져오는 것을 확인할 수 있다. 그래서 React를 참조하지 않아 오류는 나지 않지만 css가 적용되지 않는다.

{
  "presets": [
    [
      ...,
    ["@babel/preset-react", { "runtime": "automatic" }]
  ]
}
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
var _jsxRuntime = require("react/jsx-runtime");
var _react = require("@emotion/react");
...
const Test = () => {
  return /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
    css: _ref,
    children: "Test"
  });
};
var _default = exports.default = Test;

📘 + 또다른 해결 방법(권장)

이전에 물어봤던 팀원에게 블로그 글을 공유했더니 새로운 사실을 알게 되었다. 공식문서를 다시 읽어보니 @emotion/babel-preset-css-prop 은 jsx를 classic 방식으로 변환했을 때 사용하는 것이였다. 최상단에 적혀있는데 영어로 되어있어서 사용법만 찾다보니 이렇게 되었다. 앞으로는 공식문서를 더 꼼꼼히 읽어야겠다....

@emotion/babel-plugin 을 적용하니 var _react = require("@emotion/react"); 라는 불필요한 import가 사라졌고 정상적으로 동작한다.

.babelrc.json

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "> 2%, not dead",
        "useBuiltIns": "usage",
        "corejs": "3.37",
        "shippedProposals": true
      }
    ],
    "@babel/preset-typescript",
    ["@babel/preset-react", { "runtime": "automatic", "importSource": "@emotion/react" }]
  ],
  "plugins": ["@emotion"]
}

dist/Test.js

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
var _jsxRuntime = require("@emotion/react/jsx-runtime");
...
const Test = () => {
  return (0, _jsxRuntime.jsx)("div", {
    css: {
      color: 'hotpink',
      '&:hover': {
        color: 'darkorchid'
      }
    },
    children: "Test"
  });
};
var _default = exports.default = Test;

😎 문제 해결

@emotion/babel-preset-css-prop 을 preset/react와 presset/typescript보다 앞에 배치해서 해결

@emotion/babel-plugin 을 설정하여 해결

emotion 공식 문서를 읽었을 때 @emotion/babel-preset-css-prop 가 preset-react, preset-typescript 이후에 추가되어야한다고 해석하여 이게 문제가 될 거라고 생각을 못했는데, 앞에 위치하니까 해결되었다.
영어 해석 문제인지 모르겠지만 디버깅하는 과정을 통해 babel이 jsx를 해석하는 코드를 알 수 있었고, importSource 속성의 유무에 따라 변경되는 부분을 확인할 수 있었다.

@emotion/babel-preset-css-prop includes the emotion plugin. The @emotion/babel-plugin entry should be removed from your config and any options moved to the preset. If you use @babel/preset-react or @babel/preset-typescript ensure that @emotion/babel-preset-css-prop is inserted after them in your babel config.

https://emotion.sh/docs/@emotion/babel-preset-css-prop
https://emotion.sh/docs/@emotion/babel-plugin

profile
단단한 프론트엔드 개발자가 되고 싶은

0개의 댓글