Component Typing in React

TypeScript는 React에서의 컴포넌트 타이핑을 강력하게 할 수 있도록 돕는다. 이번 글에서는 TypeScript를 이용한 React 컴포넌트 타이핑 방법을 다양하게 알아본다.

Basic

가장 기본적인 형태의 클래스 React Component 타입 정의다. React.Component라는 클래스에 제네릭을 사용해서 React에서 사용하는 Props와 State의 타입 정의를 할 수 있다. Props와 State는 각각 생략이 가능하며, 생략할 경우 디폴트 값은 {} 타입이다.

다음은 Stateless Component(SFC)의 타입 정의다. SFC는 문자 그대로 State를 가지지 않기 때문에 State의 타입 정의를 할 필요가 없다. 따라서 React.StatelessComponent라는 인터페이스에 Props를 표현하는 제네릭만 넣어주면 된다. 이 때 React.StatelessComponentReact.SFC로 대체해도 된다. 취향에 맞게 사용하면 된다.

Spreading Props

기본 JSX Element인 <button/>에 몇가지 스타일을 정의해, 새로운 컴포넌트 <Button/>을 만든다고 가정해보자. 이 경우 당연히 onClick 이나 type 등, 기본적으로 JSX Element가 내장하고 있는 프로퍼티를 사용해야 할 것이다. 정적 타입 검사를 하지 않는 JavaScript에서는 Spreading Operator(...)를 이용해서 내려오는 props를 그대로 아래 컴포넌트에 복사해주면 되었다.

이 방법은 React 공식 문서에도 잘 나와있다. 하지만 TypeScript를 사용하면 이 방법을 그대로 쓸 수 없다.

그 이유는 우리가 정의한 ButtonPropsonClick이 정의되어있지 않기 때문이다. 이 경우 그냥 ButtonPropsonClick 메소드를 추가로 정의해서 해결할 수도 있지만 모든 문제가 완전히 해결되지는 않는다. 만약 type을 쓰고 싶다면? id는? disabled는? aria-label은? 그 때마다 쓰고싶은 프로퍼티를 추가해주는 건 여간 귀찮은 일이 아닐 것이다.

Definitely Typed의 React 타입 정의(@types/react)에는 여기에 필요한 모든 타입 정의가 포함되어 있다. 필요한 타입 정의가 버튼이라면, ButtonHTMLAttributes를 사용하면 되고, 인풋이라면 InputHTMLAttributes를 쓰면 된다. 결국 다음과 같이 수정할 수 있다.

위처럼 ButtonProps는 내장된 타입정의를 extends로 상속하고 추가로 필요한 프로퍼티를 정의하면 된다. 제네릭으로 넘겨준 HTMLButtonElementonClick등의 이벤트 핸들러로 넘어올 실제 DOM 객체의 타입이다.

하지만 이렇게 타입 정의를 상속하는 경우 내가 예상하지 못했던 프로퍼티가 넘어올 가능성도 생각해야 한다. 예를 들어, <Button/> 컴포넌트에는 style 프로퍼티를 넘길 수 있게 되었다. 이 때 colorstyle 프로퍼티를 동시에 넘기는 경우 color는 무시당하게 될 것이다. 따라서, 이렇게 프로퍼티를 Spreading할 경우에는 컴포넌트 내부에서 프로퍼티를 다룰 때 세심한 주의를 기울여야 한다.

Omit으로 이벤트 핸들러 다시 정의하기

Omit은 TypeScript 2.8 버전에 들어간 Conditional Types 기능을 조금 확장하여 사용하는 타입 헬퍼다. 특정 인터페이스에서 원치 않는 프로퍼티를 제외할 수 있으며 더 자세한 내용은 안도형님이 쓰신 글을 참조하자. 이 문단에서는 Omit을 사용하여 기본 이벤트 핸들러의 타입을 내가 원하는대로 재정의하는 방법을 알아본다.

다음과 같은 케이스를 고려해보자.

위 코드에서 <Button/> 컴포넌트는 기본 onClick 이벤트를 재정의했다. 본래 onClick 이벤트는 이벤트 핸들러에 이벤트 객체를 전달하도록 되어있지만 위 코드에서는 중간에서 handleClick으로 onClick 이벤트를 가로채고 프로퍼티에 저장되어 있는 값을 대신 전달했다. 이런 패턴은 React에서는 매우 흔히 쓰이는 패턴이다.

위 코드에 TypeScript를 적용하는 경우, 그리고 아까 언급한 Spreading Props를 적용하는 경우 문제가 생긴다. 기본 onClick 이벤트와 재정의한 onClick 이벤트의 타입이 서로 맞지 않기 때문이다.

앞서 말했던 대로, onClick 이벤트의 이벤트 핸들러로는 이벤트 객체가 넘어가야 하지만, 위에 새로 만든 <Button/>에서는 value를 넘기고 있고, 이 value의 타입이 이벤트 객체의 타입과는 맞지 않으므로 타입 에러를 발생시킨다.

Omit을 사용해서 이런 문제를 해결할 수 있다. 정확히는 Omit으로 재정의할 이벤트의 타입 정의를 빼준 다음, 내가 원하는 대로 새로 타입 정의를 넣어주면 되는 것이다.

여기서는 이벤트 핸들러를 다시 정의하는 유즈케이스를 다루었지만, 이벤트 핸들러 뿐만이 아니라 사용자 본인이 원하는 대로 기본 프로퍼티를 다시 정의할 수 있다. 예를 들면 value를 내가 원하는 데이터 형태만 받도록 변경할 수도 있다.

Component / Namespace 합성

TypeScript에서 클래스나 함수에 namespace를 합성하는 건 꽤 흔한 패턴이다. TypeScript 공식 문서에서도 이를 다루고 있다.

React의 컴포넌트는 크게 보아 두 가지, 클래스이거나 함수이기 때문에 마찬가지로 namespace를 결합할 수 있다. 예를 들어 다음과 같은 패턴으로 결합할 수 있다.

이 패턴은 위처럼 해당 컴포넌트의 PropsType 따위의 타입을 외부에서 사용해야할 때 유용하다. 이렇게 하면 컴포넌트에서 사용하는 각종 인터페이스나 타입들을 모아서 한 번에 하나의 엔트리로 모아서 내보낼 수 있다. 일일이 export 해주는 것과 코드량에 큰 차이는 없지만, import를 일일이 해줄 필요가 없다는 점에서 편의성이 크게 개선된다. 특히 컴포넌트 별로 index.ts 파일을 가지는 패턴의 경우, 두 번 export 하는 불편함이 줄어든다.

enum의 경우는 특히, 값과 타입이 동시에 정의되는 문법이므로, type으로 타입을 내보내는 동시에, 컴포넌트 내부에 static 키워드를 이용해서 값도 내보내야 한다는 점에 유의하자.

다음은 함수형 컴포넌트의 결합방법이다.

TypeScript의 namespace는 변수와의 합성을 지원하지 않는다. 따라서 함수 표현식을 사용할 수 없고, 함수 선언식을 사용하여 함수를 선언해야만 namespace와의 합성을 할 수 있다. 보통은 React에서 컴포넌트를 만들 때는 함수 표현식을 많이 사용하기 마련인데, namespace 합성을 해야 할 때는 함수 선언식이 강제되므로 아쉬운 부분이라고 할 수 있다.

React에서는 기본적으로 컴포넌트 하나당 대부분 하나의 인터페이스(Props)가 따라가기 때문에 막상 namespace를 쓰기로 마음 먹으면 대부분의 컴포넌트에 적용할 수 있게 된다.

마치며

개인적인 의견이지만, React에 TypeScript가 어울리지 않는다는 것은 이제 옛말인 것 같다. 아직은 부족한 점이 남아있지만, TypeScript 커뮤니티는 React를 강력하게 지원하고 있다는 사실 하나만큼은 분명해보인다. 얼마전에 릴리즈된 TypeScript 3.0 RC에서는 React의 defaultProps지원이 추가되었다. 그 전에도 요즘 TypeScript 릴리즈 개선사항을 보면 거의 React에 대한 직접적인 지원이 하나 정도는 포함이 되어있다. 따라서 앞으로도 컴포넌트 타이핑이 더욱 개선될 수 있을거라 기대해 볼 수 있다.

현재의 TypeScript만으로도 PropTypes를 사용할 때보다는 더욱 강력한 타이핑을 해 볼 수 있다. 앞서 다룬 방법들만 적용해도 기본적인 컴포넌트 타이핑을 개선하는데 도움이 될 것이다. 또한 React에 대한 지원이 강화되고있기 때문에 앞으로는 그 차이가 더욱 벌어질 것이다. React에 TypeScript가 안 어울린다고 생각했던 개발자라면 지금부터라도 시도해보는 것은 어떨까?