Skip to content

DOCS. Woowahan Components

edegil edited this page Aug 29, 2021 · 5 revisions

Woowahan Components

styled-components의 카피 버전입니다. 1주차때 부터 마지막 프로젝트에서 직접 만들어서 해야겠다 생각하고 조금씩 만들어 오고 있다가 이번 프로젝트에 와서야 완성이 된 녀석입니다.

woowahan

CSS-in-JS가 적용된 컴포넌트를 만들어주는 기능입니다.

import woowahan from 'lib/woowahan-components';

const Styled = woowahan.div`
  color: ${props => props.theme?.color};
`;

이 기능을 구현하기 위해서 Tagged Templates 기능을 사용했는데요, JS에서 제공하는 Template Literals를 함수로 파싱할 수 있게 해주는 기능입니다.

기본적인 사용 방법은 아래와 같습니다.

const parseString = (strings: string[], ...exps: (((props: any) => string) | string | number)[]): string => {
  return strings
    .map((string, i) => {
      // exps의 타입에 따라 처리를 합니다.
      // 아래에는 함수라고 치고 이야기 하겠습니다.
      return `${string}${exps[i]()}`;
    })
    .join('');
};

템플릿 리터럴에 있는 ${}를 만나게 되면 그 이전까지의 문자열을 하나의 조각으로 나누고, ${}내부의 표현식(함수, 변수 등)을 하나의 조각으로 생각합니다. 이런 조각들이 순서대로 있는데, 결국 ${} 내부의 조각들을 처리해주고 ${}가 아닌 나머지 문자열들과 순서대로 조합을 해서 새로운 문자열을 꺼내는 방식입니다.

그리고 woowahan.div와 같이 사용하기 위해서 HTML에 있는 거의 모든 태그를 다 불러와서 넣어줬는데요, 엄청 많았습니다.(물론 대부분 사용하지는 않겠지만요)

const tags = [
  'a',
  'abbr',
  'address',
  // ...
  'var',
  'video',
  'wbr',
];

woowahan.div를 불러오면, 먼저 템플릿 리터럴을 사용해서 스타일 문자열을 만들고 스타일을 적용해줍니다.

스타일 적용방법

여기서 스타일을 적용하기 위한 방법이 두가지가 있었습니다.

  • 만들어서 반환해줄 리액트 엘리면트의 style 속성에 다 때려넣는 방법
  • HTML의 CSSStyleSheet를 사용하는 방법

첫번째는 개발자도구에서 확인할 때 스타일 때문에 요소의 길이가 너무 길어지는 것 같아 두번째 방법을 선택했습니다. 두번째 방법은 스타일 시트가 있어야 했는데요, css파일이나 html<style>태그를 하나 넣어주는 방법이 있었습니다.

파일을 하나 만드는 것 보다 HTML 파일에 태그를 하나 추가해주는게 더 사용하기 편할 것 같아서 그렇게 적용을 했습니다.

const parsedString = parseString(styleString, exps, { theme, ...props });

let className = '';

const isAlreadyInserted = classMap.has(parsedString);

if (isAlreadyInserted) {
  className = classMap.get(parsedString) as string;
} else {
  className = `wc-${generateId()}`;
  classMap.set(parsedString, className);
}

const preprocessedStyle = serialize(compile(`.${className} {${parsedString}}`), stringify);
const styleList = splitStyleString(preprocessedStyle);

const styleSheet = document.styleSheets[0];

if (!isAlreadyInserted) {
  styleList.forEach(style => {
    styleSheet.insertRule(style, styleSheet.cssRules.length);
  });
}

코드입니다.

여기에서 두가지 라이브러리를 사용해줬습니다. nanoidstylis입니다.

  • nanoid는 만들어지는 컴포넌트의 클래스 명을 설정해주기 위해서 사용했는데요, styled-components로 컴포넌트를 만들어내면, sc-i2Y92q처럼 클래스가 적용되는데 이렇게 무작위로 생성되는 클래스 이름을 넣기 위해서 사용했습니다.

  • stylis는 nested된 css 문자열을 파싱해주기 위해서 사용했습니다. 역할자체는 sassstylus와 같은데, 용량이 가장 적어서 채택했습니다.

nanoid로 만든 클래스 이름과 stylis가 파싱한 CSS 문자열을 하나로 합쳐서 스타일시트에 넣어줍니다. 이때 스타일 시트의 문자열을 분리해서 넣어줘야 하는데, CSSStyleSheet는 스타일을 배열처럼 가지고 있습니다.

CSSStyleSheet {
  cssRules: CSSRuleList[
    0: CSSStyleRule {selectorText: string, style: CSSStyleDeclaration,}
    1: CSSStyleRule {selectorText: string, style: CSSStyleDeclaration,}
    2: CSSStyleRule {selectorText: string, style: CSSStyleDeclaration,}
  ]
}

위는 축소시킨 예시입니다. selectorTextstyle을 가지고 있죠? 그래서 스타일을 하나씩 분리해서 넣어줘야 합니다.

CSSStyleSheet.insertRule

CSSStyleSheet.insertRule이라는 함수는 선택자가 있어야 합니다. 무슨 말이냐 하면, 아래 코드를 보면 알 수 있습니다.

// Error😵
styleSheet.insertRule('font-size: 10px', styleSheet.cssRules.length);
// Success😊
styleSheet.insertTule('div { font-size: 10px }', styleSheet.cssRules.length);
// styleSheet.cssRules.length는 적용되는 스타일의 순서를 맞추기 위해 넣어줬습니다.

이렇게하면 CSSStyleSheet가 원하는대로 한번에 하나의 스타일을 넣을 수 있겠죠?

이렇게 스타일까지 적용을 해줬으면, 만들어진 클래스 이름과 div태그 문자열, React.createElement 메소드를 사용해서 div인 리액트 엘리먼트를 만들어내고 반환을 해줍니다.

createGlobalStyle

사용자체는 styled-componetscreateGlobalStyle과 동일합니다.

구현 방법도 위와 크게 다르지 않습니다. 다만 클래스 이름 없이 바로 전역에 스타일을 적용해준다는 점이 다르죠.

ThemeProvider

테마 기능입니다.

테마는 Context API를 사용했습니다. 그래서 ThemeProviderwoowahan-componets를 사용하는 최상단에 먼저 넣어줘야 합니다.

그리고 useTheme이라는 내부의 간단한 훅을 통해서 Tagged Template으로 받아온 함수에 넣어주는 방법으로 실행됩니다. useTheme은 개발자가 직접 사용할 일은 없습니다.

정리

  1. Tagged Template을 사용해서 스타일 문자열을 만들어 낸다.
  2. CSSStyleSheet.insertRule()을 사용해서 스타일 문자열을 적용해준다. (한번에 하나의 스타일만 넣어준다)
  3. 똑같은 클래스와 태그를 가진 리액트 컴포넌트를 반환한다.
  4. createGlobalStyle은 하나의 엘리먼트가 아니라 그냥 스타일시트에만 스타일을 적용한다.
  5. ThemeContext API를 사용해줘서 props.theme으로 사용할 수 있다.

아쉬운점

컴포넌트가 만들어지고 반환할때 타입이 React.ReactElement로 반환하지만, React의 기본적인 props(onClick, key 등..)가 자동완성이 되지 않습니다.(Typescript임에도..!) Typescript를 사용할때 제네릭으로 넘겨주게 만들어야 싶다가도 실제 React에서는 그렇지 않은데 왜 나오지 않을까? 라는 의문이 들었습니다. 하지만 이 고민만 계속하면 프로젝트가 진행이 되지 않기 때문에 왜 그렇게 되는지는 잘 모르겠습니다. JSX로 props를 넘겨준것이나 React.createElement로 만든것이나 같은데도 아직은 잘 모르겠습니다..

Clone this wiki locally