TypeStyle 사용하기

다음 프로젝트에서 본격적으로 타입스크립트를 사용한 리액트 애플리케이션을 도입하고자 하는데, 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>

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


Dohyung Ahn (rinae)
Dohyung Ahn (rinae)
삽질을 하고, 글을 남기면서 다른 사람들과 함께 자라고 싶어하는 프론트엔드 개발자입니다. 더 좋은 코드와 설계를 항상 고민하며 지식을 어떻게 효율적으로 습득하고, 어떻게 잘 나눌 수 있을지도 고민합니다.

GitHubTwitterFacebook