react에 typescript 적용시 styled-components 이슈

August 22, 2018

data에 대한 type 정의 및 다양한 잠재적 오류를 예방하기 위해 요새는 모든 react 프로젝트들을 typescript를 이용하여 개발하고 있습니다. 개발하는데에 있어서 input 과 output의 명확함. 명세가 필요없는 type 정의 및 type error 및 linting을 이용한 오류 예방적인 측면에서 typescript는 여러모로 도움이 되고 있습니다.

그러나, 몇가지 부분에서는 개발하는데 어려움을 겪습니다. 예를들면, 저는 주로 component styling을 위해서 styled-components를 자주 사용합니다. styled-components에는 component 생성시에 props를 받아와 유동적으로 style 값을 조정해줄 수 있는데, props 내의 데이터의 타입이 명확해야 하는 typescript의 특징상, props를 이용하여 유동적으로 스타일을 변경해주는게 가끔 복잡하긴 합니다.

예를들면, 이런경우들이 많습니다.

const styles = {
	container: styled.View`
		width: ${(props) => props.isLarge ? '600' : '200'}px; 
	`
};

const Example = (props) => {
	return (
		<styles.container isLarge={true} />
	)
}

해당 예시의 경우 styled-components를 이용하여, width값을 isLarge라는 prop을 통해서 유동적으로 세팅해주고 있습니다. 이를 호출하는 Component내에서는 isLarge prop을 전달해주고 있습니다. 이를 통해서 container의 width값을 prop을 통해 유동적으로 스타일링해주고 있습니다.

허나, 이를 typescript에서 그대로 적용하려 한다면, 다음과 같은 error를 반환합니다.

[ts] Property 'isLarge' does not exist on type 'ThemedStyledProps<ViewProps, any>'.

typescript에서는 이렇게 말하고 있습니다. 지금 너가 정의한 container라는 styled component내에 prop에 isLarge라는 property를 정하지 않았어!! 즉, styled component 내의 prop에 isLarge라는 type및 변수를 정적으로 지정해주지 않았으므로, typescript내에서는 에러를 반환합니다.

이를 수정하려면, interface를 통해 styled component에서 사용할 prop을 정의하거나, any type으로 무조건 변환해 주는 방법이 있지요..

// interface를 이용한 방법
interface ITest {
	isLarge: boolean,
}

const styles = {
	container: styled.View`
		width: ${(props: ITest) => props.isLarge ? '600' : '200'}px; 
	`
};

// props를 any type으로 지정
const styles = {
	container: styled.View`
		width: ${(props: any) => props.isLarge ? '600' : '200'}px; 
	`
};

물론 이렇게 하면 문제가 해결되나, styled component내에 props function에 interface에 대한 부분을 모두 지정해주어야 에러가 발생하지 않습니다. 이는 불필요하게 interface를 계속해서 선언해주기 때문에 좋은 방법은 아니라고 생각합니다.

관련된 문제에 대해서 이슈를 검색해보다가, stackoverflow에서 괜찮은 해결을 내놓은 글이 있어 적용해보고자 했습니다.

// styledUtil.ts
import { ThemedStyledFunction } from 'styled-components';

/**
 * styled-component with props util
 */
const withProps = <U>() => <P, T, O>(fn: ThemedStyledFunction<P, T, O>) =>
    fn as ThemedStyledFunction<P & U, T, O & U>;

export default withProps;

해당 source를 보니 generic을 통해서 union type 을 지정해주어 styled-components 내의 ThemedStyledFunction을 return하는 function을 생성합니다. 이를 통해서, interface로 받는 모든 type에 대한 prop을 받아서 ThemedStyledFunction에 재바인딩 해주는 형태인거죠 :) 나중에 좀더 깊게 다룰 예정이지만, typescript 내에서 function에 참조하는 변수의 type이 유동적인 경우, 제네릭을 사용하여 해당 type을 받아서 유연하게 처리할때 사용합니다.

이를 이용해서 적용하고자 할때, util을 만들었으니 util을 이용해서 interface를 통한 styled-component를 유동적으로 정의하도록 하겠습니다.

import styledUtil from 'path/to/styledUtil'

interface ITest {
	isLarge: boolean,
	isLong: boolean,
}

const styles = {
	container: styledUtil<ITest>()(styled.View)`
		width: ${(props) => props.isLarge ? '600' : '200'}px;
		height: ${(props) => props.isLong ? '500' : '100'}px;
	`
};

withProps function에서 interface type을 generic으로 설정한 function을 호출하고, return된 function에 설정하고자 하는 styled component를 대입해서 하나의 interface를 사용하여 props를 처리할 수 있습니다. 이를 통해서 width, height에 필요한 styled component 값을 미리 정의하여, 한번에 사용할 수 있게 되었습니다.

...