๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

๐Ÿชฒ debug

[jest + rtl + monorepo + next + ts]SyntaxError : Unexpected token '<' (ft. "jsx": "react-jsx")

ํ•ด๋‹น ์—๋Ÿฌ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ์€ ๋‹ค์Œ์˜ ์„ค์ •์„ ๊ธฐ์ค€์œผ๋กœ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

package manger
pnpm: "@9.0.0"

engines

node: ">=20"

dependencies
โœš "classnames": "^2.5.1"
โœš  "next": "^14"
โœš "react": "^18.3"
โœš  "react-dom": "^18.3"

dev dependencies
โœš "@jest/globals": "^29.7.0"
โœš "@testing-library/jest-dom": "^6.4.8"
โœš "@testing-library/react": "^16.0.0"
โœš "@types/jest": "^29.5.12"
โœš "@types/node": "^20"
โœš "@types/react": "^18.3.3"
โœš  "@types/react-dom": "^18.3.0"
โœš "eslint": "^8"
โœš "eslint-config-next": "15.0.0-rc.0"
โœš "jest": "^29.7.0"
โœš "jest-environment-jsdom": "^29.7.0"
โœš "jest-scss-transform": "^1.0.3"
โœš "node-sass": "^9.0.0"
โœš"sass": "^1.77.6"
โœš"ts-jest": "^29.2.3"

ํด๋” ๊ตฌ์„ฑ
    ๐Ÿ“ apps
              ใ„ด ๐Ÿ“ storybook
                      ใ„ด ๐Ÿ“ web
    ๐Ÿ“ packages
             ใ„ด๐Ÿ“design-system
    ๐Ÿ“„ turbo.json
    ๐Ÿ“„ package.json
    ... ์ƒ๋žต...

๐Ÿ‘ฉ๐Ÿป‍๐Ÿ”ง ๋ฌธ์ œ ์ƒํ™ฉ

https://turbo.build/repo/docs/guides/tools/jest์˜ ๋ฌธ์„œ๋Œ€๋กœ jest ์„ค์ • ์ค‘ ์œ„ ๊ทธ๋ฆผ๊ณผ ๊ฐ™์€ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

 

Jest | Turborepo

Learn how to use Jest in a Turborepo.

turbo.build

ts ํŒŒ์ผ๋“ค์€ ํ…Œ์ŠคํŠธ๊ฐ€ ์ž˜ ๋˜๋ฏ€๋กœ ๋ฌธ์ œ๋Š” tsx, ์ฆ‰ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋‹ค๋ฃจ๋Š” ํ…Œ์ŠคํŠธ ํŒŒ์ผ๋“ค์— ์žˆ์—ˆ๋‹ค.

์™ธ๋ถ€์—์„œ importํ•ด์˜จ ์ปดํฌ๋„ŒํŠธ๋“ค ๋ชจ๋‘ render ํ•  ์ˆ˜ ์—†๋Š” ์ƒํ™ฉ.

 

๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ ์ž ํ•ด๋‹น ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋กœ ๊ตฌ๊ธ€๋ง์„ ์—ด์‹ฌํžˆ ํ–ˆ์ง€๋งŒ ํ˜„์žฌ ๋‚˜์˜ ํ”„๋กœ์ ํŠธ์™€ ๋งž๋Š” ํ•ด๊ฒฐ์ฑ…์ด ์—†์—ˆ๋‹ค.

https://stackoverflow.com/questions/51994111/jest-encountered-an-unexpected-token
https://imygnam.tistory.com/80

 

โ˜‘๏ธ babel์ด ์•„๋‹Œ ts-jest ์‚ฌ์šฉ ์ค‘ : babel์€ ํŠธ๋žœ์ŠคํŒŒ์ผ ๋„๊ตฌ์ด๋ฏ€๋กœ ๋”ฐ๋กœ ์„ค์ •์„ ํ•ด์ฃผ์ง€ ์•Š์œผ๋ฉด ํƒ€์ž…์ฒดํฌ๋ฅผ ํ•ด์ฃผ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ„๊ฒฐ์„ฑ์„ ์œ„ํ•ด ts-jest ์„ ํƒ

๐Ÿ€ ์ƒํ™ฉ ํŒŒ์•…

๋จผ์ € ์ง€๊ธˆ๊นŒ์ง€ ์„ค์ •ํ•ด ์คฌ๋˜ ๊ฑฐ๋ฅผ ์‚ดํŽด๋ณด์ž.

์„ค์ •

 

๐Ÿ“ web

 

๐Ÿ“„ jest.config.json

{
  "preset": "ts-jest",
  "testEnvironment": "jsdom",
  "testMatch": ["**/__tests__/**/*.ts?(x)", "**/?(*.)+(test).ts?(x)"],
  "transform": {
    "^.+\\.ts?$": "ts-jest",
    "^.+\\.tsx?$": "ts-jest",
    "^.+.test.tsx?": "ts-jest",
    "^.+\\.scss$": "jest-scss-transform"
  },
  "moduleDirectories": ["node_modules"]
}

 

๐Ÿ“„ . eslintrc.mjs

/** @type {import("eslint").Linter.Config} */

export default {
  root: true,
  extends: ["@tripie/eslint-config/next.js"],
  parser: "@typescript-eslint/parser",
  parserOptions: {
    project: true,
  },
  overrides: [
    {
      files: ["/components/*/__tests__/**/*"],
      env: {
        jest: true,
      },
    },
  ],
};

 

๐Ÿ“„ tsconfig.json

{
  "extends": "@tripie/typescript-config/react-library.json",
  "jsdom": "react",
  "compilerOptions": {
    "plugins": [
      {
        "name": "next"
      }
    ],
    "outDir": "dist",
    "jsx": "preserve"
  },
  "include": [
    "next-env.d.ts",
    "next.config.mjs",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts"
  ],
  "exclude": ["node_modules", "dist"]
}

 

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ

ใ„ด ๐Ÿ“„ components/__test__/themeButton.test.tsx

import { expect } from "@jest/globals";
import { cleanup, render } from "@testing-library/react";
import "@testing-library/jest-dom";
import "@testing-library/jest-dom/jest-globals";
import { Text } from "@tripie/design-system";

afterEach(cleanup);

it("text has 'os control'", () => {

  const { getByText } = render(<Text>os control</Text>);
  expect(getByText("os control")).toBeInTheDocument();
  
});

 

Text Component : @tripie/design-system/src/typography/text/_test.tsx

"use client";

import classNames from "classnames/bind";
import Style from "./_text.module.scss";

const style = classNames.bind(Style);

export interface TextProps {
  dim?: boolean;
  size?:
    | "default"
    | "h0"
    | "h1"
    | "h2"
    | "h3"
    | "h4"
    | "text"
    | "small"
    | "tiny";
  color?: "primary" | "secondary" | "danger" | "warning" | "gray" | "emphasize";
  bold?: boolean;
  children: string;
  className?: string;
}

function Text({ children, className, ...props }: TextProps) {
  const splitText = `${children}`.split("\n").map((sentence, index) => {
    return (
      <span className={style(className, "text")} key={index + sentence}>
        {sentence}
      </span>
    );
  });
  return <div>{splitText}</div>;
}

export default Text;

 

๐Ÿ“ design-system

 

๋‚˜๋จธ์ง€ ์„ค์ •์€ ๋™์ผํ•œ๋ฐ ๋‹ค๋ฅธ ๊ฑด

 

๐Ÿ“„ tsconfig.json

{
  "extends": "@tripie/typescript-config/react-library.json",
  "compilerOptions": {
    "outDir": "dist"
  },
  "include": ["typings", "src"],
  "exclude": ["node_modules", "dist"],
  "jsx": "react-jsx"
}

 

 

์—ฌ๊ธฐ์„œ ํžŒํŠธ๋ฅผ ์–ป์–ด์„œ "jsx" ์„ค์ •์„ ๋ณ€๊ฒฝํ•ด ๋ดค๋‹ค.

๐Ÿ€ ํ•ด๊ฒฐ

jsx:"preserve"์—์„œ jsx: "react-jsxdev"๋กœ ๋ณ€๊ฒฝํ•ด ์คฌ๋‹ค.

{
  "extends": "@tripie/typescript-config/react-library.json",
  "jsdom": "react",
  "compilerOptions": {
    "plugins": [
      {
        "name": "next"
      }
    ],
    "outDir": "dist",
    "jsx": "react-jsxdev" // ๋ณ€๊ฒฝ
  },
  "include": [
    "next-env.d.ts",
    "next.config.mjs",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts"
  ],
  "exclude": ["node_modules", "dist"]
}

 

๐Ÿค” ํ•ด๊ฒฐ์€ ๋๋Š”๋ฐ, ์™œ ๋œ ๊ฑฐ์ง€?

 

ts ๊ณต์‹ ๋ฌธ์„œ์—์„œ jsx ์„ค์ •์— ๋”ฐ๋ผ ์–ด๋–ป๊ฒŒ ๋‹ฌ๋ผ์ง€๋Š”์ง€ ์‚ดํŽด๋ณด์ž.

๊ณต์‹ ๋ฌธ์„œ์— ์˜ํ•˜๋ฉด JSX๊ฐ€ ์–ด๋–ป๊ฒŒ ๋ณ€๊ฒฝ๋ ์ง€๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋Š” ์„ค์ •์ด๋ผ๊ณ  ํ•œ๋‹ค.

์ฆ‰,. tsx ํ™•์žฅ์ž๋กœ ๋œ ํŒŒ์ผ๋“ค์„ js๋กœ ์–ด๋–ป๊ฒŒ ๋ณ€๊ฒฝํ•ด ์ค„ ์ง€์— ๋Œ€ํ•œ ์„ค์ •์ด๋‹ค. ๊ฐ€๋Šฅํ•œ ์„ค์ •์€ ๋‹ค์Œ ํ‘œ๋กœ ์ •๋ฆฌํ–ˆ๋‹ค.

export const HelloWorld = () => <h1>Hello world</h1>; ๋ผ๋Š” ์ฝ”๋“œ๊ฐ€ ์žˆ๋‹ค๊ณ  ํ•˜๋ฉด

"react-jsx" .js ํŒŒ์ผ๋“ค์„ _jsx ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ js ํŒŒ์ผ๋กœ ๋ฐฉ์ถœ. PROD ํ™˜๊ฒฝ์— ์ตœ์ ํ™”
import { jsx as _jsx } from "react/jsx-runtime";
export const HelloWorld = () => _jsx("h1", { children: "Hello world" });
 
"react-jsxdev" .js ํŒŒ์ผ๋“ค์„ _jsx ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ js ํŒŒ์ผ๋กœ ๋ฐฉ์ถœํ•œ๋‹ค๋Š” ์ ์€ react-jsx์™€ ๋™์ผํ•˜์ง€๋งŒ, DEV ํ™˜๊ฒฝ์—์„œ๋งŒ
import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";
const _jsxFileName = "/home/runner/work/TypeScript-Website/TypeScript-Website/packages/typescriptlang-org/index.tsx";

export const HelloWorld = () => _jsxDEV("h1", { children: "Hello world" }, void 0, false, { fileName: _jsxFileName, lineNumber: 9, columnNumber: 32 }, this);
 
"preserve" .jsx ํŒŒ์ผ๋“ค์„ jsx ๋ณ€๊ฒฝ์—†์ด ๋ฐฉ์ถœ
import React from 'react';
export const HelloWorld = () => <h1>Hello world</h1>;
"react-native" .jsx ํŒŒ์ผ๋“ค์„ jsx ๋ณ€๊ฒฝ์—†์ด ๋ฐฉ์ถœ
import React from 'react';
export const HelloWorld = () => <h1>Hello world</h1>;
"react" .js ํŒŒ์ผ๋“ค์„ jsx React.createElement ํ˜ธ์ถœ๊ณผ ๋™๋“ฑํ•œ ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋ฐฉ์ถœ
import React from 'react';
export const HelloWorld = () => React.createElement("h1", null, "Hello world");

 

preserve๋กœ ์„ค์ •ํ–ˆ์„ ๋•Œ๋Š” ๋™์ž‘ํ•˜์ง€ ์•Š์•˜์ง€๋งŒ "react-jsx"๋‚˜ "react-jsxdev๋กœ ์„ค์ •ํ•ด ์คฌ์„ ๋•Œ ๋™์ž‘ํ–ˆ๋‹ค.

("react"๋กœ ์„ค์ • ์‹œ ์•„๋ž˜์™€ ๊ฐ™์€ ์—๋Ÿฌ ๋ฐœ์ƒ)

components/__test__/themeButton.test.tsx:34:33 - error TS2686: 'React' refers to a UMD global, but the current file is a module. Consider adding an import instead.

 

 

 

์ด๋Š” jest๊ฐ€ ํŒŒ์ผ ํŒŒ์‹ฑ์„ ํ•  ์ˆ˜ ์—†์–ด์„œ ๋ฐœ์ƒํ–ˆ๋˜ ๋ฌธ์ œ๋‹ค. ๊ทธ๋ƒฅ jsx ํŒŒ์ผ(preserve)์ด ์•„๋‹ˆ๋ผ jest๊ฐ€ ์ธ์‹ํ•  ์ˆ˜ ์žˆ๋Š” ์ฝ”๋“œ๋กœ ts ์„ค์ •์„ ๋ณ€๊ฒฝํ•ด ์ฃผ๋‹ˆ (react-jsxdev) ์ž˜ ๋™์ž‘ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.


๐Ÿ“š References

https://turbo.build/repo/docs/guides/tools/jest

 

TSConfig Reference - Docs on every TSConfig option

From allowJs to useDefineForClassFields the TSConfig reference includes information about all of the active compiler flags setting up a TypeScript project.

www.typescriptlang.org

https://www.typescriptlang.org/tsconfig/#jsx

 

Jest | Turborepo

Learn how to use Jest in a Turborepo.

turbo.build