Skip to content

TypeStyle 사용하기

Published:

다음 프로젝트에서 본격적으로 타입스크립트를 사용한 리액트 애플리케이션을 도입하고자 하는데, CSS를 입히기 위한 도구로 TypeStyle을 활용하는 방안을 검토중이다.

그래서 egghead의 강좌를 가볍게 따라가며 내용을 정리해보았다. 이 정도면 충분히 도입할 만한 가치가 있어 보인다.

적어도 스타일 속성 이름으로 오타가 날 일은 없어 보이며 JS를 적극적으로 활용하여 동적인 스타일을 입힐 수 있으리라 기대한다.

참고로 아래의 예제 코드는 모두 리액트 애플리케이션을 개발한다는 전제하에 소개하고 있다. 하지만 TypeStyle 자체는 어느 환경에서나 사용 가능하다.

기본 사용법

import { style } from 'typestyle';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
const className = style({
color: 'red',
position: 'relative'
});
ReactDOM.render(
<div className={className}>
Hello TypeStyle!
</div>,
document.getElementById('root')
);

style 함수로 생성된 내용이 스타일이 적용된 임의의 클래스 이름을 만들게 되고 DOM은 바로 클래스 이름을 사용하면 되도록 구성되어 있다.

이 과정에서 객체 형식으로 되어있는 key-value 값은 이미 라이브러리에 CSS 스펙에 맞게 정의되어 있기 때문에 오타가 날 시 컴파일러가 친절하게 에러를 잡아준다.

스타일 믹스인

// 위 코드와 동일한 DOM 랜더링
function fontSize(value: number | string) {
const valueStr = typeof value === "string" ? value : value + "px";
return {
fontSize: valueStr,
};
}
const fontColor = { color: "red" };
const className = style(fontSize("3em"), fontColor);
// ...

위의 코드 처럼 style 함수는 객체를 믹스인 할 수 있다. 프로퍼티가 맞는 순수 객체가 들어가면 아무 없이 작동하기 때문에 위의 fontSize 함수처럼 상황에 맞게 적절한 객체를 리턴하는 함수를 만들어서 다양한 상황에 맞게 사용할 수 있다.

미디어 쿼리

media 함수로 간단히 미디어 쿼리를 생성할 수 있다. 생성된 쿼리를 style 함수 안에 넣으면 자동으로 Nested 쿼리 형식으로 생성된다. 수동으로 Nested 쿼리를 입력하고자 할 때는 style 함수 안에 넣는 객체에 $nest 속성을 사용하면 된다.

// ...
import { style, media } from "typestyle";
const className = style(
{
color: "red",
transition: "font-size .2s",
},
media({ minWidth: 500, maxWidth: 700 }, { fontSize: "30px" }),
media({ minWidth: 701 }, { fontSize: "50px" })
);
// ...

수도 클래스(pseudo class) 작성

Sass, Less와 유사한 형태로 계층 화된 쿼리를 작성할 수 있다. 위에서 언급한 $nest 속성을 선택하면 해당 클래스의 계층 쿼리를 작성할 수 있다.

const className = style({
color: "red",
transition: "font-size .2s",
$nest: {
"&:focus": {
// .class:focus
fontSize: "30px",
},
"&&:hover": {
// .class.class:focus
fontSize: "50px",
},
},
});
// ...

CSS 클래스 조합하기

클래스를 조합하는건 style 함수로 만들어진 클래스들을 분기에 따라 추가하고 제거하도록 만들면 된다. 다만 이 과정에서 있는 경우와 없는 경우에 따라 문자열을 조합하는게 번거로울 수 있는데, classes 함수가 귀찮은 부분을 쉽게 해결하도록 도와준다.

const baseClassName = style(
{ color: '#333' }
);
const errorClassName = style(
{ backgroundColor: 'red' }
);
interface AppProps {
className?: string;
hasError?: boolean;
}
const App = ({ className, hasError }: AppProps) => (
<div className={
classes(
baseClassName,
className,
hasError && errorClassName
)
}>
Hello world
</div>
);
// ...

Keyframes 함수로 애니메이션 만들기

import { style, keyframes } from "typestyle";
const colorAnimationName = keyframes({
"0%": { color: "black" },
"50%": { color: "blue" },
});
const className = style({
fontSize: "20px",
animationName: colorAnimationName,
animationDuration: "1s",
animationIterationCount: "infinite",
});
// ...

colorAnimationName 을 일일이 변수로 분리 할 필요는 없다. 한번만 사용할거면 animationName 속성에 바로 keyframes 를 사용한 객체를 리턴하도록 만들면 된다.

일반 CSS를 사용하기

cssRaw 함수를 사용하고 그 안에 일반 CSS를 문자열로 집어넣으면 그대로 글로벌 스타일로 변환된다. 그리고 그 파일안에 있는 컴포넌트에 전부 영향을 준다. 특정 이름을 가진 클래스를 만들고, 간단하게 기존 스타일을 마이그레이션 하거나 NormalizeCSS 등을 바로 가져올 때도 유용하게 사용할 수 있다.

import { style, cssRaw } from 'typestyle';
cssRaw(`
.red {
color: red;
}
`);
cssRaw(`
.bold {
font-weight: bold;
}
`);
const className = style(
{ fontSize: '30px' }
);
ReactDOM.render(
<div className={className + ' red bold'}> // red, bold 클래스 사용 가능
Hello world
</div>,
document.getElementById('root')
);

구형 브라우저용 속성 사용하기

rgba 같은 구형 브라우저에서 지원되지 않는 속성을 사용하면서 구형 브라우저를 지원하는 용도로 rgb 를 사용하는 경우, CSS 파일에서는 보통 같은 속성을 두번 작성해서 문제를 해결할 수 있다. 하지만 TypeStyle은 객체를 파싱하기 때문에 같은 속성 값을 두번 선언할 수는 없다.

하지만 배열을 사용하면 한 속성에 배열의 요소를 순차적으로 따라 스타일을 적용하도록 만들 수 있으며, 비슷한 원리를 vendor prefix에도 적용할 수 있다.

import { style, types } from "typestyle";
const scroll: types.NestedCSSProperties = {
"-webkit-overflow-scrolling": "touch",
overflow: "auto",
};
const className = style(scroll, {
fontSize: "30px",
backgroundColor: [
"rgb(200, 54, 54)", // 구형 브라우저용
"rgba(200, 54, 54, 0.5)", // 요즘 브라우저용
],
});
// ...

정적 페이지 만들어보기

getStyles 함수는 현재 작성된 파일 안에 정의된 TypeStyle 스타일을 문자열로 변환하는 기능을 한다. 이를 이용해서 간단한 HTML 페이지를 만들 수 있다.

app.tsx
import * as fs from 'fs';
import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';
import { style, getStyles } from 'typestyle';
const className = style({
color: 'red',
fontSize: '30px'
});
const App = () => (
<div className={className}>
Hello World
</div>
);
const html = ReactDOMServer.renderToStaticMarkup(<App />);
const css = getStyles();
const renderPage = ({ html, css }: { html: string; css: string; }) => `
<html>
<head>
<style>${css}</style>
</head>
<body>
${html}
</body>
</html>
`;
const renderedPage = renderPage({ html, css });
fs.writeFileSync(__dirname + '/index.html', renderedPage);
<!-- 결과물 -->
<html>
<head>
<style>
.fyuerk {
color: red;
font-size: 30px;
}
</style>
</head>
<body>
<div class="fyuerk">Hello World</div>
</body>
</html>

위 방식을 응용하여 서버 사이드 랜더링에서도 스타일을 적용할 수 있다. 영상 참고.