다음 프로젝트에서 본격적으로 타입스크립트를 사용한 리액트 애플리케이션을 도입하고자 하는데, 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 페이지를 만들 수 있다.
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>
위 방식을 응용하여 서버 사이드 랜더링에서도 스타일을 적용할 수 있다. 영상 참고.