그럼에도 불구하고

👨‍💻

[TypeScript] JSX란? 본문

TypeScript/TypeScript basics

[TypeScript] JSX란?

zenghyun 2023. 5. 24. 11:37

 

오늘은 JSX에 대해 알아보겠습니다.

 

[ JSX ] 

JSX는 내장형 XML 같은 구문입니다. 변환의 의미는 구현에 따라 다르지만 유효한 JavaScript로 변환되어야 합니다. JSX는 React로 큰 인기를 얻었지만, 이후 다른 구현도 등장했습니다. TypeScript는 임베딩, 타입 검사, JSX를 JavaScript로 직접 컴파일하는 것을 지원합니다. 

 

 

[ 기본 사용법 (Basic usage) ]

JSX를 사용하려면 다음 두 가지 작업을 해야 합니다.

 

1. 파일 이름을. tsx 확장자로 지정합니다.

2. jsx 옵션을 활성화 합니다.

 

TypeScript는 preservereact 및 react-native라는 세 가지의 JSX 모드와 함께 제공됩니다. 이 모드들은 방출 단계에서만 영향을 미치며, 타입 검사에는 영향을 받지 않습니다. 

 

preserve 모드는 다른 변환 단계(예: 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

 

--jsx 명령줄 플래그 또는 tsconfig.json 파일의 해당 옵션을 사용하여 모드를 지정할 수 있습니다.

 

*참고: React JSX를 생성할 때 --jsxFactory 옵션으로 사용할 JSX 팩토리(JSX factory) 함수를 지정할 수 있습니다 (기본값은 React.createElement)

 

[ as 연산자 (The as operator) ]

타입 단언(type assertion)을 어떻게 작성하는지 떠올려 볼까요?

var foo = <foo>bar;

위 코드는 변수 bar가 foo 타입이라는 것을 단언합니다. TypeScript는 꺾쇠괄호를 사용해 타입을 단언하기 때문에, JSX 구문과 함께 사용할 경우 특정 문법 해석에 문제가 될 수도 있습니다. 결과적으로 TypeScript는. tsx 파일에서 화살 괄호를 통한 타입 단언을 허용하지 않습니다.

위의 구문은. tsx파일에서 사용할 없으므로, as라는 대체 연산자를 통해 타입 단언을 해야 합니다. 위의 예시는 as 연산자로 쉽게 다시 작성할 있습니다.

var foo = bar as foo;

as 연산자는. ts와. tsx 파일 모두 사용할 수 있으며, 꺾쇠괄호 형식의 단언과 동일하게 동작합니다.

 

[ 타입 검사 (Type Checking) ]

JSX의 타입 검사를 이해하기 위해선, 먼저 내장 요소와 값-기반 요소의 차이점에 대해 알아야 합니다. JSX 표현식 <expr />에서 expr은 환경에 내장된 요소(예: DOM 환경의 div 또는 span) 혹은 사용자가 만든 사용자 정의 컴포넌트를 참조할 것입니다. 이는 다음과 같은 두 가지 이유로 중요합니다.

  1. 리액트에서 내장 요소는 React.createElement("div")과 같은 문자열로 생성되는 반면, 사용자가 만든 컴포넌트는 React.createElement("MyComponent")가 아닙니다.
  2. JSX 요소에 전달되는 속성의 타입은 다르게 조회되어야 합니다. 내장 요소의 속성은 내재적으로 알고 있어야 하지만, 컴포넌트는 각각 자신의 속성 집합을 지정하려고 합니다.

TypeScript는 React와 동일한 규칙을 사용하여 구별합니다. 내장 요소는 항상 소문자로 시작하고 값-기반 요소는 항상 대문자로 시작합니다.

 

[ 내장 요소 (Intrinsic elements) ]

내장 요소는 특수 인터페이스 JSX.IntrinsicElements에서 조회됩니다. 기본적으로 인터페이스가 지정되지 않으면 그대로 진행되어 내장 요소 타입은 검사 되지 않습니다. 그러나 인터페이스가 있는 경우, 내장 요소의 이름은 JSX.IntrinsicElements 인터페이스의 프로퍼티로 조회됩니다. 아래의 예제를 보겠습니다

 

1
2
3
4
5
6
7
8
declare namespace JSX {
    interface IntrinsicElements {
        foo: any
    }
}
 
<foo />// 성공
<bar />// 오류
cs

 

위의 예제에서 <foo />는 잘 동작하지만, <bar />는 JSX.IntrinsicElements에 지정되지 않았기 때문에 오류를 일으킵니다.

 

 

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

값-기반 요소는 해당 스코프에 있는 식별자로 간단하게 조회됩니다.

 

1
2
3
4
import MyComponent from "./myComponent";
 
<MyComponent />// 성공
<SomeOtherComponent />// 오류
cs

 

값-기반 요소를 정의하는 데엔 다음의 두 가지 방법이 있습니다:

  1. 함수형 컴포넌트 (FC)
  2. 클래스형 컴포넌트

이 두 가지 유형의 값-기반 요소는 JSX 표현식에서 서로 구별할 수 없으므로, TS는 과부하 해결을 사용하여 먼저 함수형 컴포넌트 표현식으로 해석합니다. 이 과정이 성공적으로 진행되면, TS는 이 선언을 표현식으로 해석합니다. 함수형 컴포넌트로 해석되지 않는다면, TS는 클래스형 컴포넌트로 해석을 시도합니다. 이 과정도 실패할 경우, TS는 오류를 보고합니다.

 

 

[ 함수형 컴포넌트 (Function Component) ]  

컴포넌트는 첫 번째 인수가 props 객체인 JavaScript 함수로 정의됩니다. TS는 컴포넌트의 반환 타입이 JSX.Element에 할당 가능하도록 요구합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
interface FooProp {
  name: string;
  X: number;
  Y: number;
}
 
declare function AnotherComponent(prop: {name: string});
function ComponentFoo(prop: FooProp) {
  return <AnotherComponent name={prop.name/>;
}
 
const Button = (prop: {value: string}, context: { color: string }) => <button>
cs

 

함수형 컴포넌트는 JavaScript 함수이므로, 함수 오버로드 또한 사용 가능합니다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 {
  ...
}
cs

 

 

[ 속성 타입 검사 (Attribute type checking) ]

속성 타입 검사를 위해선 첫 번째로 요소 속성 타입을 결정해야 합니다. 이는 내장 요소와 값-기반 요소 간에 약간 다른 점이 있습니다. 내장 요소의 경우, 요소 속성 타입은 JSX.IntrinsicElements의 프로퍼티 타입과 동일합니다.

 

1
2
3
4
5
6
7
8
declare namespace JSX {
  interface IntrinsicElements {
    foo: { bar?: boolean }
  }
}
 
// 'foo'의 요소 속성 타입은 '{bar?: boolean}'
<foo bar />;
cs

 

값-기반 요소의 경우엔 조금 더 복잡합니다. 이전에 요소 인스턴스 타입 의 프로퍼티 타입에 따라 결정됩니다. 사용할 프로퍼티는 JSX.ElementAttributesProperty에 따라 결정됩니다. 이는 단일 프로퍼티로 선언되어야 합니다. 이후 해당 프로퍼티 이름을 사용합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
declare namespace JSX {
  interface ElementAttributesProperty {
    props; // 사용할 프로퍼티 이름을 지정
  }
}
 
class MyComponent {
  // 요소 인스턴스 타입의 프로퍼티를 지정
  props: {
    foo?: string;
  }
}
 
// 'MyComponent'의 요소 속성 타입은 '{foo?: string}'
<MyComponent foo="bar" />
cs

 

요소 속성 타입은 JSX에서 속성 타입을 확인하는 데 사용됩니다. 선택적 혹은 필수적인 프로퍼티들이 지원됩니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
declare namespace JSX {
  interface IntrinsicElements {
    foo: { requiredProp: string; optionalProp?: number }
  }
}
 
<foo requiredProp="bar" />// 성공
<foo requiredProp="bar" optionalProp={0/>// 성공
<foo />// 오류, requiredProp이 누락됨
<foo requiredProp={0/>// 오류, requiredProp은 문자열이어야 함
<foo requiredProp="bar" unknownProp />// 오류, unknownProp은 존재하지 않음
<foo requiredProp="bar" some-unknown-prop />// 성공, 'some-unknown-prop'은 유효한 식별자가 아니기 때문에
cs

 

추가적으로, JSX.IntrinsicAttributes 인터페이스는 일반적으로 컴포넌트의 props나 인수로 사용되지 않는 JSX 프레임워크를 위한 추가적인 프로퍼티를 지정할 수 있습니다. - 예를 들면 React의 key. 더 나아가서, JSX.IntrinsicClassAttributes<T> 제네릭 타입을 사용하여 클래스형 컴포넌트에 대해 동일한 종류의 추가 속성을 지정할 수 있습니다 (함수형 컴포넌트 제외하고). 이 유형에서, 제네릭의 매개변수는 클래스 인스턴스 타입에 해당합니다. React의 경우, 이는 Ref<T> 타입의 ref 속성을 허용하는 데에 쓰입니다. 일반적으로는, JSX 프레임워크 사용자가 모든 태그에 특정 속성을 제공할 필요가 없다면, 이런 인터페이스의 모든 프로퍼티는 선택적이어야 합니다.

 

스프레드 연산자 또한 동작합니다.

1
2
3
4
5
6
var props = { requiredProp: "bar" };
<foo {...props} />// 성공
 
var badProps = {};
<foo {...badProps} />// 오류
 
cs

 

[ 자식 타입 검사 (Children Type Checking) ]

TypeScript 2.3부터, TS는 자식의 타입 검사를 도입했습니다. 자식 은 자식 JSX 표현식을 속성에 삽입하는 요소 속성 타입의 특수한 프로퍼티입니다. TS는 JSX.ElementAttributesProperty를 사용해 props를 결정하는 것과 유사하게, JSX.ElementChildrenAttribute를 사용해 해당 props 내의 자식의 이름을 결정합니다. JSX.ElementChildrenAttribute는 단일 프로퍼티로 선언되어야 합니다.

 

1
2
3
4
5
declare namespace JSX {
  interface ElementChildrenAttribute {
    children: {};  // 사용할 자식의 이름을 지정
  }
}
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div>
  <h1>Hello</h1>
</div>;
 
<div>
  <h1>Hello</h1>
  World
</div>;
 
const CustomComp = (props) => <div>{props.children}</div>
<CustomComp>
  <div>Hello World</div>
  {"This is just a JS expression..." + 1000}
</CustomComp>
 
cs

 

다른 속성처럼 자식 의 타입도 지정할 수 있습니다. 예를 들어 React 타이핑을 사용하는 경우 기본 타입을 오버라이드 할 것입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 성공
<Component name="foo">
  <h1>Hello World</h1>
</Component>
 
// 오류 : 자식은 JSX.Element의 배열이 아닌 JSX.Element 타입입니다.
<Component name="bar">
  <h1>Hello World</h1>
  <h2>Hello World</h2>
</Component>
 
// 오류 : 자식은 JSX.Element의 배열이나 문자열이 아닌 JSX.Element 타입입니다.
<Component name="baz">
  <h1>Hello</h1>
  World
</Component>
cs

[ JSX 결과 타입 (The JSX result type) ]

기본적으로  JSX 표현식의 결과물은 any 타입입니다. JSX.Element 인터페이스를 수정하여 특정한 타입을 지정할 수 있습니다. 그러나 이 인터페이스에서는 JSX의 요소, 속성, 자식에 대한 정보를 검색할 수 없습니다.

 

 

[ 표현식 포함하기 (Embedding Expressions) ]

 JSX 중괄호( { } ) 표현식을 감싸 태그 사이에 표현식 사용을 허용합니다

 

1
2
3
var a = <div>
  {["foo""bar"].map(i => <span>{i / 2}</span>)}
</div>
cs

위의 코드는 문자열을 숫자로 나눌 수 없으므로 오류를 일으킵니다. preserve 옵션을 사용할 때, 결과는 다음과 같습니다. 

 

1
2
3
var a = <div>
  {["foo""bar"].map(function (i) { return <span>{i / 2}</span>; })}
</div>
cs

 

 

[ 리액트와 통합하기 (React integration) ]

리액트에서 JSX를 사용하기 위해선 React 타이핑을 사용해야 합니다. 이는 리액트를 사용할 수 있도록 JSX 네임스페이스를 적절하게 정의합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// <reference path="react.d.ts" />
 
interface Props {
  foo: string;
}
 
class MyComponent extends React.Component<Props, {}> {
  render() {
    return <span>{this.props.foo}</span>
  }
}
 
<MyComponent foo="bar" />// 성공
<MyComponent foo={0/>// 오류
cs

 

 

[ 팩토리 함수 (Factorty Functions) ]

jsx: react 컴파일러 옵션에서 사용하는 팩토리 함수는 설정이 가능합니다. 이는 jsxFactory 명령 줄 옵션을 사용하거나 인라인 @jsx 주석을 사용하여 파일별로 설정할 수 있습니다. 예를 들어 jsxFactory에 createElement를 설정했다면 <div />는 React.createElement("div") 대신 createElement("div")로 생성될 것입니다.

 

주석 pargma 버전은 다음과 같이 사용할 수 있습니다. 

 

1
2
3
import preact = require("preact");
/* @jsx preact.h */
const x = <div />;
cs

 

이는 다음처럼 생성됩니다.

 

1
2
const preact = require("preact");
const x = preact.h("div"null);
cs

 

선택된 팩토리는 전역 네임스페이스로 돌아가기 전에 JSX 네임스페이스(타입 검사를 위한 정보)에도 영향을 미칩니다. 팩토리가 React.createElement(기본값)로 정의되어 있다면, 컴파일러는 전역 JSX를 검사하기 전에 React.JSX를 먼저 검사할 것입니다. 팩토리가 h로 정의되어 있다면, 컴파일러는 전역 JSX를 검사하기 전에 h.JSX를 검사할 것입니다.

Comments