블로그 쓰는게 좀 무서워서 계속 미뤄왔는데, 나만의 일기를 기록한다는 느낌으로 가볍게 쓰려고 마음먹고
처음으로 글을 쓴다. 오늘의 주제는 React Styled Component 를 구현하기다.
근데 아직 Styled Component 사용법도 잘 몰라서 그것부터 확인한다.
import React from 'react';
import styled from 'styled-components';
const Circle = styled.div`
  width: 5rem;
  height: 5rem;
  background: black;
  border-radius: 50%;
`;
function App() {
  return <Circle />;
}
export default App;https://react.vlpt.us/styling/03-styled-components.html 에서 간단한 예제를 가져왔다.
우선 이런 형태로 쓰는걸 목표로 시작해본다.
먼저 깃허브에 프로젝트를 만들었다.
https://github.com/cothis/styled-component


아무것도 없는 빈 프로젝트 모습..
기존에 만들어놨던 보일러플레이트 코드가 있어서 가져오고 약간 수정했다.

이때 styled.div, styled.span 처럼 쓸수 있게 만들어야된다.
우선은 css 는 생각하지말고 컴포넌트 생성만이라도 할 수 있게 해보자.
styled라는 객체의 div, span 등등의 key에 tagged template 함수를 넣어줘야한다.
그리고 taggedTemplate의 반환결과는 컴포넌트여야한다.

일단 첫단계로 div 만 만들어볼 수 있게 해보았다.


음..이렇게 하니까 안되네.. 다른 방법을 찾아본다.

styled 의 생성부분을 변경했더니 App.tsx의 에러도 사라지고 웹페이지에서도 렌더링이 된다.

근데 이렇게 했더니 중간의 하이하이 글자가 표시되지 않는다.. props에서 children을 넣어줘야할것 같다.

이렇게 변경해줬더니


렌더가 된다.
먼저 construct 를 감싸는 constructWithTag 함수를 만들었다. 이 함수는 tag 스트링을 인자로 받아서
그 인자이름으로 CustomTag를 생성해주는 함수를 반환한다.
import React, { ReactNode } from 'react';
interface Props {
  children: ReactNode;
}
const constructWithTag = (tag: string) => {
  const CustomTag = `${tag}` as keyof JSX.IntrinsicElements;
  const construct = (
    strings: TemplateStringsArray,
    ...args: any[]
  ): Function => {
    const NewComponent = (props: Props) => {
      return <CustomTag>{props.children}</CustomTag>;
    };
    return NewComponent;
  };
  return construct;
};
const styled = {
  div: constructWithTag('div'),
  span: constructWithTag('span'),
  a: constructWithTag('a'),
  form: constructWithTag('form'),
  button: constructWithTag('button'),
};
export default styled;
/* App.tsx */
import styled from '../lib/styled-components/styled-components';
const Div = styled.div``;
const Button = styled.button``;
const A = styled.a``;
const App = () => {
  return (
    <div>
      React Boiler Plate
      <Div>
        <Button>버튼</Button>
        <A href="www.naver.com">앵커태그</A>
      </Div>
    </div>
  );
};
export default App;이처럼 만들었을때, 
렌더가 잘 되는걸 확인했다.
그러나, A태그의 href속성이 먹히질 않는걸 확인했다.
이걸 적용되도록 한번 바꿔본다.
const constructWithTag = (tag: string) => {
  const CustomTag = `${tag}` as keyof JSX.IntrinsicElements;
  const construct = (
    strings: TemplateStringsArray,
    ...args: any[]
  ): Function => {
    const NewComponent = (props: Props) => {
      return <CustomTag {...props}>{props.children}</CustomTag>;
    };
    return NewComponent;
  };
  return construct;
};{...props} 를 통해 CustomTag에 props를 넘겨줬다.

/* dom-elmements.ts */
export default [
  'a',
  'abbr',
  'address',
  'area',
  'article',
  'aside',
  'audio',
  'b',
  'base',
  'bdi',
  'bdo',
  'big',
  'blockquote',
  'body',
  'br',
  'button',
  'canvas',
  'caption',
  'cite',
  'code',
  'col',
  'colgroup',
  'data',
  'datalist',
  'dd',
  'del',
  'details',
  'dfn',
  'dialog',
  'div',
  'dl',
  'dt',
  'em',
  'embed',
  'fieldset',
  'figcaption',
  'figure',
  'footer',
  'form',
  'h1',
  'h2',
  'h3',
  'h4',
  'h5',
  'h6',
  'head',
  'header',
  'hgroup',
  'hr',
  'html',
  'i',
  'iframe',
  'img',
  'input',
  'ins',
  'kbd',
  'keygen',
  'label',
  'legend',
  'li',
  'link',
  'main',
  'map',
  'mark',
  'marquee',
  'menu',
  'menuitem',
  'meta',
  'meter',
  'nav',
  'noscript',
  'object',
  'ol',
  'optgroup',
  'option',
  'output',
  'p',
  'param',
  'picture',
  'pre',
  'progress',
  'q',
  'rp',
  'rt',
  'ruby',
  's',
  'samp',
  'script',
  'section',
  'select',
  'small',
  'source',
  'span',
  'strong',
  'style',
  'sub',
  'summary',
  'sup',
  'table',
  'tbody',
  'td',
  'textarea',
  'tfoot',
  'th',
  'thead',
  'time',
  'title',
  'tr',
  'track',
  'u',
  'ul',
  'var',
  'video',
  'wbr',
  // SVG
  'circle',
  'clipPath',
  'defs',
  'ellipse',
  'foreignObject',
  'g',
  'image',
  'line',
  'linearGradient',
  'mask',
  'path',
  'pattern',
  'polygon',
  'polyline',
  'radialGradient',
  'rect',
  'stop',
  'svg',
  'text',
  'tspan',
];dom Elements 리스트를 styled component github에서 구해왔다.

위 형태로 domElements 배열 항목들을 동적으로 넣어줬다.
import styled from '../lib/styled-components/styled-components';
const Div = styled.div``;
const Button = styled.button``;
const A = styled.a``;
const H1 = styled.h1``;
const App = () => {
  return (
    <div>
      React Boiler Plate
      <Div>
        <Button>버튼</Button>
        <A href="https://www.naver.com">앵커태그</A>
        <H1>안녕하세요</H1>
      </Div>
    </div>
  );
};
export default App;위처럼 작성하고 테스트해보니...

이렇게 렌더링이 된다!
다만 아쉬운건, 이렇게 동적으로 추가해줬을때, styled. 눌렀을때 자동완성이 안뜬다.. 이건 해결방법이 없을까??
명확한 목표
parsing을 위해 stylis 라이브러리를 설치했다.
yarn add stylis @types/stylis
stylis 라이브러리의 주요 기능은 compile, serialize, stringify 이다.
이걸 이용해서 클래스의 내용물을 만들어서, script 태그에 넣어주도록 해본다.
이떄 태그명을 앞에 클래스명으로 붙여보겠다.
const stylis = (tag: string, content: string) =>
  serialize(compile(`.${tag}{${content}}`), stringify);일단은 tagged template으로 들어오는 값을 변수가 없다고 생각하고 처리한다. 문자열로 변환해준다.
const css = strings.map((string, i) => `${string}${args[i] ?? ''}`).join('');그 후에, css를 class에 들어갈 스트링으로 바꿔본다.
const classString = stylis(tag, css);

이런식으로 .div div{background-color:red;} 가 나오는걸 확인할 수 있다.
이제 이 내용물을 style 태그를 만들어서 넣어주고, header에 붙여본다.

그리고 CustomTag의 클래스네임에 해당 tag를 넣어줘봤다.
테스트해보니 안되서, 확인했더니 

이런식으로 변경해줘야 했다. 이 태그 자체에 붙는 스타일이니까...
그렇게 하면 결론은

이 상태로도 꽤 만족할만큼 결과물이 나왔다. keyframe, class 만들어주는 기능 완료
그런데, 아직 아쉬운점은 props를 넘겨줄수가 없다!

이 사진처럼 H1 woowa라는 속성을 props에 넘겨주려고 한다.
기존 코드에서 태그드 템플릿 처리(class string 만드는 과정)을 컴포넌트 외부에서 했더니, Props에 접근할 수가 없었다.
저 콜백함수에서 Props를 써야하기 때문에 접근해야 한다. 그렇기 때문에, 이를 Component의 useEffect 안으로 옮겨왔다.
const constructWithTag = (tag: string) => {
  const CustomTag = `${tag}` as keyof JSX.IntrinsicElements;
  const construct = (
    strings: TemplateStringsArray,
    ...args: any[]
  ): Function => {
    const NewComponent = (props: Props) => {
      useEffect(() => {
        const css = strings
          .map((string, i) => {
            let arg = args[i] ?? '';
            if (arg instanceof Function) {
              console.log(arg, props);
              arg = arg(props);
            }
            return `${string}${arg}`;
          })
          .join('');
        const classString = stylis(tag, css);
        const $style = document.createElement('style');
        $style.innerHTML = classString;
        document.querySelector('head')?.appendChild($style);
        return () => {
          $style.remove();
        };
      }, []);
      return (
        <CustomTag {...props} className={tag}>
          {props.children}
        </CustomTag>
      );
    };
    return NewComponent;
  };
  return construct;
};이것처럼 useEffect 안으로 오니까 문제가 해결되었다.
이제 또다른 문제는..

이 에러를 해결해야한다.
Warning: Received 'true' for a non-boolean attribute 'woowa'.
에러 해결전에..
기존 Styled 타입을 이렇게 변경했다.
type Styled = Record<
  typeof domElements[number],
  ReturnType<typeof constructWithTag>
>;prop 중에 해당 dom Element의 Attribute 중에 존재하는것만 따로 빼서 dom에 넣어주도록 코드를 변경했다.
const domProps: { [key: string]: any } = {};
      const $dom = document.createElement(tag);
      Object.keys(props).forEach((prop) => {
        if (prop in $dom) {
          domProps[prop] = props[prop];
        }
      });
      return (
        <CustomTag {...domProps} className={tag}>
          {props.children}
        </CustomTag>
      );tag를 입력하지 않았을때, 클래스명없이 css 적용을 시켜주기 위한 기능을 추가했다.
export const createGlobalStyle = constructWithTag(); 로 만들었고 이를 추가하기 위해
tag에 undefined 일때에 대한 처리를 추가해줬다.
import React, { ReactNode, useEffect, AllHTMLAttributes } from 'react';
import domElements from './dom-elements';
import { compile, serialize, stringify } from 'stylis';
interface Props {
  children: ReactNode;
  [key: string]: any;
}
const constructWithTag = (tag?: string) => {
  const CustomTag = `${tag ?? 'div'}` as keyof JSX.IntrinsicElements;
  const stylis = (tag: string | undefined, content: string) => {
    if (!tag) {
      return serialize(compile(`${content}`), stringify);
    } else {
      return serialize(compile(`.${tag}{${content}}`), stringify);
    }
  };
  const construct = (
    strings: TemplateStringsArray,
    ...args: any[]
  ): Function => {
    const NewComponent = (props: Props) => {
      useEffect(() => {
        const css = strings
          .map((string, i) => {
            let arg = args[i] ?? '';
            if (arg instanceof Function) {
              console.log(arg, props);
              arg = arg(props);
            }
            return `${string}${arg}`;
          })
          .join('');
        const classString = stylis(tag, css);
        const $style = document.createElement('style');
        $style.innerHTML = classString;
        document.querySelector('head')?.appendChild($style);
        return () => {
          $style.remove();
        };
      }, []);
      const domProps: { [key: string]: any } = {};
      if (tag) {
        const $dom = document.createElement(tag);
        Object.keys(props).forEach((prop) => {
          if (prop in $dom) {
            domProps[prop] = props[prop];
          }
        });
        $dom.remove();
      }
      return (
        <CustomTag {...domProps} className={tag}>
          {props.children}
        </CustomTag>
      );
    };
    return NewComponent;
  };
  return construct;
};
type Styled = Record<
  typeof domElements[number],
  ReturnType<typeof constructWithTag>
>;
const styled: Styled = {};
domElements.forEach((domElement) => {
  styled[domElement] = constructWithTag(domElement);
});
export default styled;
export const createGlobalStyle = constructWithTag();매번 생성때마다 증가되는 숫자값이 필요하다.
이 숫자값을 가지고 적절한 hash 처리후 문자열로 변경하여 class명에 붙여준다.
간단히 최상단에 sequence = 1을 주고, stylis 함수내에서 sequnce++을 함으로써 값을 증가시킬 수 있고, 이걸 문자열로 변환처리를 해보겠다.
import React, { ReactNode, useEffect } from 'react';
import domElements from './dom-elements';
import { compile, serialize, stringify, hash } from 'stylis';
let sequence = 1;
interface Props {
  children: ReactNode;
  [key: string]: any;
}
const constructWithTag = (tag?: string) => {
  const CustomTag = `${tag ?? 'div'}` as keyof JSX.IntrinsicElements;
  const stylis = (className: string | undefined, content: string) => {
    if (!className) {
      return serialize(compile(`${content}`), stringify);
    } else {
      return serialize(compile(`.${className}{${content}}`), stringify);
    }
  };
  const construct = (
    strings: TemplateStringsArray,
    ...args: any[]
  ): Function => {
    const NewComponent = (props: Props) => {
      const suffix = `${sequence}`;
      const className = tag ? tag + '-' + suffix : '';
      sequence++;
      useEffect(() => {
        const css = strings
          .map((string, i) => {
            let arg = args[i] ?? '';
            if (arg instanceof Function) {
              arg = arg(props);
            }
            return `${string}${arg}`;
          })
          .join('');
        const classString = stylis(className, css);
        const $style = document.createElement('style');
        $style.innerHTML = classString;
        document.querySelector('head')?.appendChild($style);
        return () => {
          $style.remove();
        };
      }, []);
      const domProps: { [key: string]: any } = {};
      if (tag) {
        const $dom = document.createElement(tag);
        Object.keys(props).forEach((prop) => {
          if (prop in $dom) {
            domProps[prop] = props[prop];
          }
        });
        $dom.remove();
      }
      return (
        <CustomTag {...domProps} className={className}>
          {props.children}
        </CustomTag>
      );
    };
    return NewComponent;
  };
  return construct;
};
type Styled = Record<
  typeof domElements[number],
  ReturnType<typeof constructWithTag>
>;
const styled: Styled = {};
domElements.forEach((domElement) => {
  styled[domElement] = constructWithTag(domElement);
});
export default styled;
export const createGlobalStyle = constructWithTag();위처럼 sequence를 주고, dom 을 생성할때마다 className을 변경하도록 만들었다.
그다음 이걸 문자열로 바꿔주기 위해서 generateAplphabeticName이란 코드를 라이브러리에서 가져오고, 조금 수정했다.
그런다음 suffix를 만들어줄때 이 함수를 사용해서 처리했다.
/* This is the "capacity" of our alphabet i.e. 2x26 for all letters plus their capitalised
 * counterparts */
const charsLength = 52;
/* start at 75 for 'a' until 'z' (25) and then start at 65 for capitalised letters */
const getAlphabeticChar = (code: number) =>
  String.fromCharCode(code + (code > 25 ? 39 : 97));
/* input a number, usually a hash and convert it to base-52 */
export function generateAlphabeticName(code: number) {
  let name = '';
  let x;
  code = code << 27;
  /* get a char and divide by alphabet-length */
  for (x = Math.abs(code); x > charsLength; x = (x / charsLength) | 0) {
    name = getAlphabeticChar(x % charsLength) + name;
  }
  return name;
}
결과는 이렇게 나왔다!
먼저 아래 코드를 추가했다.
const ThemeProvider = {};
export { ThemeProvider };이제 이 코드를 동작할수  있도록 만들어야 한다.
ThemeProvider는 컨텍스트 api 를 활용하는것이고, 먼저 공부해보고 개발을 진행해보겠다.
코드를 보니까, css랑 DefaultTheme도 export해야되서, 둘다 export 할수있도록 코드를 변경했다.
const ThemeProvider = {};
const DefaultTheme = {};
const css = {};
export { css, DefaultTheme, ThemeProvider };이제 이걸 어떻게 채워야할까...
먼저 css는 태그드 템플릿 함수이고 추후 리턴값을 재사용할 수 있다.
그럼 문자열로 컴파일된걸 넘겨주면 안될까? 우선 그렇게 시도해보겠다.
const css = (strings: TemplateStringsArray, ...args: any[]) => {
  const result = strings.map((string, i) => `${string}${args[i] ?? ''}`).join('');
  return serialize(compile(result), stringify);
};그리고 ThemeProvider를 만들어본다.
ThemeProvider를 만들다보니 다른것도 다 필요해서 같이 만들었다.
interface DefaultTheme {
  [key: string]: any;
}
const ThemeContext = React.createContext<DefaultTheme>({});
ThemeContext.displayName = 'ThemeContext';
const ThemeProvider = (props: ProviderProps<DefaultTheme>) => {
  return <ThemeContext.Provider {...props}>{props.children}</ThemeContext.Provider>;
};그리고 중간에 argument 처리에서 함수호출부분을
arg = arg({ ...ThemeContext, ...props });처럼 변경했다.