본문 바로가기
프론트엔드

[CSS IN JS] 스타일드 컴포넌트에 관하여.

by ISA(류) 2022. 2. 7.

바닐라 js로 여러가지 기능을 만들어 보는 js-util 프로젝트를 시작하고 가벼운 DOM 조작 부터 스타일링 부터 모두 js로 처리하는 방식으로 프로젝트를 진행하다보니 스타일링 관련해서 코드를 관리하는게 매우 귀찮아졌다.

 

그걸 조금 편하게할 CSS IN JS 기능을 간단하게 개발해보는 것도 나쁘지 않을 거 같아서 뭘 기준으로 삼을까 생각해보니 스타일드 컴포넌트가 가장 쉽고 간편하게 개발 할 수 있을거 같아서 비슷하게 개발했다. 개발하다 보니 대체 스타일드 컴포넌트는 어떻게 되어 있는지 궁금하고 어떤식으로 로직을 처리하는가? 싶어서 바로 스타일드 컴포넌트 Repo를 포크하고 내용 로직을 살펴보았다. 어차피 내가 원하는 것은 바닐라로 다른 기능을 만들면서 스타일링을 스타일드 컴포넌트 처럼 간단하게 하는 것이다 보니 스타일드 컴포넌트 와 같은 정교함과 여러 기능들을 만들 필요는 없었기에 세세하게 모든 것을 파악하지는 않았지만 대략적인 로직을 파악하는 방법과 그를 통해서 만들어낸 소스 코드를 간단하게 에세이 형식으로 글을 남겨본다.

 

스타일드 컴포넌트

먼저 스타일드 컴포넌트는 lerna를 통해서 모노레포 형식으로 구성이 되어 있더라. 가이드 문서(공식문서), 샘플코드(샌드박스) 그리고 스타일드 컴포넌트 코드들을 따로 패키징 하여서 관리하기 위함으로 보인다.

해당 위치에서 package폴더에 벤치마크, 샌드박스, 우리가 원하는 소스 코드가 들어가 있다.

packages 내부 모노레포로 관리하다보니 이런 구성인듯

나머진 지금 알아야 할 필요가 없으니 소스들이 있는 styled-components 폴더에 들어가주자

styled-components내부

제스트로 여러가지 테스트 조건이 있다.(웹뿐 아니라 리액트 네이티브등) 번들링은 웹팩이 아닌 롤업을 사용하는데 config파일을 봤을때 별 차이점은 못느끼겠으니 생략한다. 패키지 의존성을 살짝 보고 지나가자

바벨과 이모션, 리액트 네이티브 지원을 위한 모듈, 객체 비교에 사용될 모듈, css전처리기를 의존하고 있다.

해당 부분을 파악한 것만으로도 이미 스타일드 컴포넌트가 대략적으로 어떤식으로 구성 되어 있을지 예상가능하다.

이제 소스코드를 직접 찾아보기 위해서 src 폴더를 들어가보자.

src내부

타입과 여러가지 기능들과 플랫폼에 맞게 쪼개어 놓은 구성이 눈에 들어온다. 그러나, 내가 스타일드를 100% 활용하겠다는 것은 아니기에 모든 걸 살펴 볼 필요는 없다. 그냥 index.ts파일을 열어보자.

스타일드 컴포넌트 설치하고 불러올 시 확인 가능한 패키지의 entry 포인트다.

createGlobalStyle 등은 base라는 파일을 통해서 정리되어서 export 되고 있고,

우리들이 불러와 쓰는 styled는 constructors 폴더의 styled라는 파일에서 시작 된다는 것을 알 수 있다. 

styled파일

해당 파일을 살펴보니 우리가 쓰는 styled가 어떻게 구성 되는지 대략적인 파악이 끝났다.

constructWithOption이라는 것을 통해서 styled 메서드를 구성하고 그걸 domElements라는 파일의 배열을 통해서 

forEach돌면서 div, a등 styled.p 메서드를 만들어준다. 그럼 utils의 domElements는 무엇인가? 그냥 HTML 태그 목록이다. 즉, 그냥 모든 태그 목록을 한번씩 돌아가면서 해당 이름을 가진 메서드를 만들어 주는 것이다.

그 내용물 자체는 createStyledComponents라는 모델을 통해서 constructWithOptions로 만들어 주는데 constructWithOptions라는 파일의 내용물은 대략 이렇다.

저기서 템플릿 function을 반환해주는데 그게 바로 스타일드 컴포넌트의 컴포넌트 반환하는 메서드다. 해당 부분의 경우 컴포넌트가 가진 여러가지 특징들을 처리해주는 부분인데 그저 바닐라로 js를 통해서 css in js 방식으로 dom을 관리 해야할 입장에서는 아직 필요가 없고 내용만 많은 부분이라 생략한다. 해당 부분을 이제 코드로 옮겨보자.

class StyledInJs {
    constructor(domElements) {
        this.cssom = [];
        this.domElements = domElements;
        this.provideElements();
    }
    provideElements() {
        this.domElements.forEach((tag) => {
            this[tag] = (styleSheets, ...args) => {
                const Components = (props) => {
                    const id = this.getId(tag);
                    const callback = args.map((el) => typeof el === 'function' ? el(props) : el);
                    const styles = styleSheets
                        .map((el, idx) => callback[idx] ? el + callback[idx] : el)
                        .join('').
                        split('&');

                    const findCss = this.checkOverCss(id, styles);
                    if (!findCss) {
                        // 선택자 처리
                        const css = styles.reduce((result, current, idx) =>
                            idx === 0 ?
                                result += `.${id} {${current}}\n` :
                                result += `.${id}${current}\n`
                            , '');
                        this.addStyle(id, css);
                    }

                    const tagElement = document.createElement(tag);
                    tagElement.className = findCss ?
                        findCss.id :
                        id;

                    return tagElement;
                }
                // 컴포넌트 방식이 아니면 불편하기만 하므로 props 기능은 제공하지 않는다..
                return Components({});
            }
        });
    }
    createGlobalStyle(args) {
        const id = this.getId('global');
        const css = `${args.join('')}`;

        if (!this.checkOverCss(id, args)) {
            this.addStyle(id, css);
        }

    }
    checkOverCss(id, styles) { // 아이디를 통해서 식별하는게 더 효율적이긴하다.
        const checkOver = this.cssom.find((css) => css.style === styles.join(''));

        if (!checkOver) {
            this.cssom.push({
                id: id,
                style: styles.join('')
            });
        }

        return checkOver;
    }
    getId(tag) {
        const random = (Math.random() * 1000).toString().replace('.', '');

        return `${tag}-${random}`;
    }
    addStyle(id, css) {
        const style = document.createElement('style');
        style.id = id;
        style.innerHTML = css;

        document.querySelector('head').appendChild(style);
    }
}

코드를 보면 알 수 있듯이 로직을 클래스로 묶어서 처리하는게 가벼운 기능을 대충 개발하기 편해서 클래스로 선언한 후 constructor 과정에서 기존 스타일드 컴포넌트와 동일하게 domElements의 배열을 메서드로 선언해줬다. 스타일시트에는 각각 random난수를 이용한 식별자를 부여하여서 서로 매칭해줬다.

스타일드 컴포넌트 자체가 각 메서드로 기존 컴포넌트나 새로운 컴포넌트를 생성해서 반환하고 그걸 사용하는 형식이다 보니 메서드를 통한 스타일드 노드는 입력 된 스타일을 각 식별자로 매칭한 노드를 반환해주는 방식으로 처리했고, 글로벌 스타일의 경우 그냥 전역 스타일을 선언하는 방식으로 간단히 처리했다.

가상 선택자의 경우도 그저 문자열 처리를 통해서 간단히 구현했다.

 

본래 스타일드 컴포넌트의 경우 각 컴포넌트가 제거될때 해당 스타일도 정리되는 부분이 있지만 솔직히 해당 부분을 구현할 필요성을 못느껴서 나는 생략했다. 실제로 사용하는 방법은 이렇다.

    const styled = new StyledInJs(domElements); // domElements = HTMLtagList
    styled.createGlobalStyle`
        button {
            border: none;
        }
        pre {
            background: tomato;
        }
    `;
    const div = styled.div`
        display: block;
        width: 100%;
        height: 100px;
        border-radius: 3px;
        padding: 0.5rem 0;
        margin: 0.5rem 1rem;
        background: red;
        &:active{
            background: tomato;
        }
    `;
    const button = styled.button`
        width: 300px;
        height: 100%;
        color: ${(props) => props.color || "gray"};
        background: cyan;
        &:hover{
            background: blue;
            color: #fff;
        }
    `;
    button.innerText = '스타일드';
    div.appendChild(button);
    document.querySelector('#app').appendChild(div);

js 함수의 경우 ()을 제외하고도 ``라는 태그 리터럴 방식으로 호출이 가능한데 이를 통해서 ${props => props.color}문자열 리터럴 문법을 사용하여서 ..args로 콜백 함수 등을 반환 할 수 있다.(${}만나면 해당 부분에서 문자열이 split되고 삽입되는 내용은 다음 인자로 넘어감) 이 부분이 styled components가 일반적인 호출이 아닌 태그드 리터럴 방식으로 호출 하는 이유이다. 

 

더 자세한 내용이 궁금한 사람들은 아래 링크를 참고하길 바란다.

 

https://github.com/styled-components/styled-components

 

GitHub - styled-components/styled-components: Visual primitives for the component age. Use the best bits of ES6 and CSS to style

Visual primitives for the component age. Use the best bits of ES6 and CSS to style your apps without stress 💅 - GitHub - styled-components/styled-components: Visual primitives for the component age...

github.com

*결과물은 아래 Repo의 4번째 항목인 styledInJs를 통해서 확인 해볼 수 있다.

https://github.com/yoonjonglyu/js-util

 

GitHub - yoonjonglyu/js-util: html, css, js, vanillajs, animation, module & util

html, css, js, vanillajs, animation, module & util - GitHub - yoonjonglyu/js-util: html, css, js, vanillajs, animation, module & util

github.com

 

반응형