나를 괴롭히는 TypeScript JSX 속성.

Nochi·2023년 10월 22일
3

TypeScript

목록 보기
1/1

이번에 프로젝트에서 테스트 코드를 추가하기 위해 @testing-library를 사용하는데 pnpm test시 에러가 발생하였다.

SyntaxError: Unexpected token '<’

“왜 이 에러가 났을까?” 하면서 찾아보다가 compilerOptions.jsxpreserve 인 것이 문제가 된다고 하여서 react로 수정하니 잘 동작하였다.

// tsconfig.json
{
...
"compilerOptions": {
	    ...
			"jsx": "react",
			...
     },
...
}

저번에도 이런 비슷한 이슈가 있었는데, 안되었던 것인지 모르는데 넘어갔다가 또 또 또 이러는 것 보니 이제는 넘어갈 수 없다…!


Documentation - JSX

Basic usage

JSX는 ECMAScript에 XML을 확장한 구문으로 트랜스파일러를 통해 ECMAScript로 변환하는데 사용된다. JSX를 사용하기 위해 두 가지 작업을 수행해야 한다.

  1. .tsx확장자를 사용하여 파일 이름을 지정.
  2. tsconfig.jsoncompilerOptions.jsx 옵션을 활성화 // 사용하라는 의미 같음

TypeScript는 세 가지(preserve, react, react-native) JSX 모드를 제공하는데 오직 출력(emit) 단계에만 영향을 미치며, 타입 체킹에는 영향을 주지 않는다.

  • preserve 모드
    • JSX를 출력의 일부로 유지하여 다른 변환 단계(예: Babel)에서 추가로 사용할 수 있게 함.
    • 출력 파일은 .jsx 확장자.
  • react 모드
    • React.createElement를 출력, 사용하기 전에 JSX 변환을 거칠 필요가 없음.
    • 출력 파일은 .js 확장자
  • react-native 모드
    • 모든 JSX를 유지한다는 점에서 'preserve'와 동일
    • 출력 파일은 .js 확장자
방법입력산출출력 파일 확장자
preserve<div /><div />.jsx
react<div />React.createElement("div").js
react-native<div /><div />.js
react-jsx<div />_jsx("div", {}, void 0);.js
react-jsxdev<div />_jsxDEV("div", {}, void 0, false, {...}, this);.js

Type Checking

JSX를 통한 타입 검사를 이해하기 위해서는 내장 요소((HTML Dom) Intrinsic elements)값 기반 요소(Value-based elements) 사이의 차이를 이해한다.

  • React에 대해, 내장 요소는 문자열로 출력(React.createElement("div")).
    • 반면에 사용자가 생성한 컴포넌트는 그렇지 않다.(React.createElement(MyComponent)).
  • JSX 요소에 전달되는 속성의 타입은 다르게 조회되어야 한다.
    • 내장 요소의 속성은 본질적으로 알려져야 하며, 컴포넌트는 자체 속성 세트를 지정하고자 할 것.

TypeScript는 이러한 구분을 위해 React가 사용하는 것과 동일한 규칙을 사용하며,

  • 내장 요소는 항상 소문자로 시작하고,
  • 값 기반 요소는 항상 대문자로 시작한다.

Intrinsic elements(내장 요소)

내장 요소는 특별한 인터페이스인 JSX.IntrinsicElements에서 조회된다. 기본적으로 인터페이스가 지정되지 않으면 그대로 진행되어 내장 함수는 타입 검사가 이루어지지 않을 것이다.

declare namespace JSX {
  interface IntrinsicElements {
    foo: any;
  }
}
<foo />; // 성공
<bar />; // 오류

Value-based elements(값-기반 요소)

값-기반 요소는 해당 스코프 내의 식별자로 간단하게 조회된다.

import MyComponent from "./myComponent";
<MyComponent />; // 성공
<SomeOtherComponent />; // 오류

값-기반 요소를 정의하는 두 가지 방법이 있다.

  • 함수형 컴포넌트 (FC)
  • 클래스형 컴포넌트

값 기반 요소(value-based elements)는 JSX 표현식에서 어떤 컴포넌트인지 구별할 수 없다. 그리하여, TypeScript는 먼저 오버로드 해석을 사용한다.

  • 표현식을 함수 컴포넌트로 해석하려고 시도한다.
  • 이 과정을 성공하면, TypeScript는 표현식의 선언으로 해석을 마친다.
  • 만약 함수 컴포넌트로 해석되지 않으면, 클래스 컴포넌트로 해석하려고 시도한다.
  • 만약 해석을 실패하면, TypeScript는 오류를 보고한다.

오버로드 해석
: 프로그래밍에서 특정 함수나 메서드가 여러 시그니처를 가질 때, 컴파일러가 적절한 함수 또는 메서드를 선택하는 과정.

TypeScript는 표현식의 선언으로 해석
: 이 표현식이 함수 컴포넌트를 나타내는 경우, TypeScript는 이 함수가 올바르게 정의되었는지, 예상된 매개변수를 받고 있는지 등을 확인하고 함수 컴포넌트로서의 정의와 일치한다면, 컴파일러는 '해석을 마친다'고 표현한다.

이 두 시도 모두 실패하면, 컴파일 오류가 발생한다. 이러한 접근 방식은 TypeScript가 컴포넌트의 타입을 올바르게 인식하고, 예상되는 속성과 메서드가 컴포넌트에 제공되는지 확인할 수 있도록 돕는다.

Function Component

컴포넌트는 자바스크립트 함수로 정의되며 그 첫 번째 인자는 props 객체이다. TypeScript는 이 함수의 반환 타입이 JSX.Element에 할당 가능해야 한다는 것을 강제한다.

해당 컴포넌트 함수는 JSX.Element 타입 또는 그것과 호환되는 타입의 값을 반환해야 함을 의미하며, 이를 통해 TypeScript는 반환 값이 JSX (React 컴포넌트를 작성하는 데 사용되는 마크업 언어) 규칙을 준수하는지 확인한다.

interface ClickableProps {
  children: JSX.Element[] | JSX.Element;
}
 
interface HomeProps extends ClickableProps {
  home: JSX.Element;
}
 
interface SideProps extends ClickableProps {
  side: JSX.Element | string;
}
 
function MainButton(prop: HomeProps): JSX.Element;
function MainButton(prop: SideProps): JSX.Element;
function MainButton(prop: ClickableProps): JSX.Element {
  // ...
}

Class Component

특정 컴포넌트의 인스턴스 타입이 JSX.ElementClass 인터페이스와 호환되지 않는 경우, TypeScript는 JSX를 사용하는 해당 코드 부분에서 오류를 발생시킨다. 이를 통해 개발자는 JSX를 사용하여 만들어진 엘리먼트가 올바른 인스턴스 생성 규칙을 따르도록 강제할 수 있으며, 이는 더 엄격한 타입 검사와 안정성을 제공한다.

declare namespace JSX {
  interface ElementClass {
    render: any;
  }
}
class MyComponent {
  render() {}
}
function MyFactoryFunction() {
  return { render: () => {} };
}
<MyComponent />; // ok
<MyFactoryFunction />; // ok
class NotAValidComponent {}
function NotAValidFactoryFunction() {
  return {};
}
<NotAValidComponent />; // error
<NotAValidFactoryFunction />; // error

Attribute type checking

속성을 타입 체크하는 첫 번째 단계는 엘리먼트 속성 타입을 결정하는 것이다. 이것은 내장 요소와 값 기반 요소 사이와 약간 다르다.

내장 요소의 경우, 이는 JSX.IntrinsicElements에 있는 속성의 타입이다.

JSX로 작성된 내장 요소(예: div, span 등의 HTML 요소)의 경우, 해당 요소가 받아들일 수 있는 속성들의 타입은 TypeScript 내장 타입인 JSX.IntrinsicElements에서 정의된 속성의 타입을 따르게 된다는 것을 의미한다.

TypeScript는 내장 요소에 전달되는 속성의 유효성을 검사할 수 있다.

이전에 결정된 요소 인스턴스 타입의 속성의 타입에 의해 결정된다.

  • 사용할 속성은 JSX.ElementAttributesProperty에 의해 결정된다. 이는 단일 속성으로 선언되어야 한다.
  • JSX.ElementAttributesProperty가 제공되지 않는 경우, 클래스 요소의 생성자 또는 함수 컴포넌트 호출의 첫 번째 매개변수의 타입이 대신 사용된다.

JSX.IntrinsicAttributes 인터페이스는 컴포넌트의 props나 인자에 일반적으로 사용되지 않는 JSX 프레임워크에서 사용되는 추가 속성을 지정하는 데 사용될 수 있다. 예를 들어 React에서의 'key'와 같은 속성이 이에 해당한다.

더 구체적으로, 제네릭 JSX.Intrinsic**Class**Attributes<T> 타입은 클래스 컴포넌트에 대해서만 (함수 컴포넌트 제외) 동일한 종류의 추가 속성을 지정하는 데 사용될 수 있다.

const props = { requiredProp: "bar" };
<foo {...props} />; // ok
const badProps = {};
<foo {...badProps} />; // error

Children Type Checking

children은 엘리먼트 속성 타입에서 특별한 속성으로, 자식 JSXExpressions이 속성에 삽입되기 위해 사용된다.

TypeScript가 JSX.ElementAttributesProperty를 사용하여 props의 이름을 결정하는 것과 유사하게, TypeScript는 JSX.ElementChildrenAttribute를 사용하여 그 props 내의 children의 이름을 결정합니다.

JSX.ElementChildrenAttribute는 단일 속성으로 선언되어야 한다.

The JSX result type

기본적으로 JSX 표현식의 결과는 any로 타입이 지정된다. JSX.Element 인터페이스를 지정함으로써 타입을 사용자 정의할 수 있다. 그러나 이 인터페이스로부터 요소, 속성 또는 JSX의 children에 대한 타입 정보를 검색하는 것은 불가능하다.

Embedding Expressions(표현식 삽입)

JSX는 중괄호({ })로 표현식을 둘러싸서 태그 사이에 표현식을 삽입할 수 있게 해준다.

위의 코드는 문자열을 숫자로 나눌 수 없기 때문에 오류를 발생시킵니다. 'preserve' 옵션을 사용할 때의 출력은 다음과 같다:

const a = (
  <div>
    {["foo", "bar"].map(function (i) {
      return <span>{i / 2}</span>;
    })}
  </div>
);

React integration

React와 함께 JSX를 사용하려면 React 타입 정의를 사용해야 합니다. 이 타입 정의는 React와 함께 사용하기에 적합하도록 JSX 네임스페이스를 적절히 정의한다.


후기

상단의 표를 통해서 jest가 읽을 수 없는 형식의 파일이기 때문에 에러가 났다는 것을 알게 되었다. 어찌보면 당연한 일이지만, 나는 당연히 변환이 된 뒤에 동작할 줄 알았는데, 그렇지 않아서 충격을 받았지만, 왜 저렇게 나뉘어있는지 알게 되어서 좋은 시간이었다.

그리고, 내부적으로 타입스크립트가 JSX를 어떻게 다루는지 알게 되어서 재밌게 써 내려갔다. 특히, FC 먼저 확인해보고 클래스 컴포넌트인지 확인한다고 했는데,

if(component-type is function?) {
	return React.FC;
}
if(component-type is class?) {
	return React.ClassComponent;
}
return throw new Error("둘 다 아니야");

세부적으로 다른 내용들이 있겠지만 문득 상상하자면 이런 느낌일까 싶었다.

르블랑의 법칙처럼, 그냥 지나쳐버리면 되돌아보지 않는 것 같으니 그때 공부 리스트를 뽑아놔야겠다.

0개의 댓글