본문 바로가기

애매한 카테고리

styled-components<💅>를 써보았다.

🤔 써보고 싶었던 이유

  • 옛날에 만든 프로젝트들을 리팩토링 할 겸 꺼내봤다가 이걸 무슨 생각으로 썼지 싶은 코드들에도 충격 받은건 둘째치고 컴포넌트 별로 따로있는 SCSS파일이 너무 보기가 싫었다. 막 개발공부 시작했을 때 만든 프로젝트라 SCSS 배워본다고 튜토리얼에서 본대로 따라 만들었었는데, 다시보니까 매우 간단한 프로젝트라서 CSS코드도 별로 없는데 굳이 별도의 파일을 만들어서 디렉토리가 지저분하게 길어지는게 보기에 좋지 않았다. 게다가 각각의 컴포넌트를 개별 폴더에 저장했었는데, import 할때 경로 길어지지말라고 index.js파일에서 컴포넌트를 다시 export해줘서 이렇게 되면 컴포넌트 하나당 총 파일이 3개(예를들면 Button.jsx, Button.scss, index.js)가됨...; 웁웁🤢 그래서 별도의 폴더도 없애고 공통 컴포넌트면 components/common폴더에 따로 넣고 그게 아니면 그냥 components/ 밑에 다 빼서 쓰기로함.
  • 회사에서 Next.js랑 styled jsx를 썼었는데, 나 같은 경우는 컴포넌트 만들 때 jsx 코드를 보면서 css도 같이 적용하는 경우가 많아서 두 파일을 왔다갔다 할 필요 없이 한 파일에서 스타일도 같이 주는게 개발하기에도 편하게 느껴졌다. 그래서 비슷한 styled-components도 한 번 써보고 싶었다.
  • <💅> 이모지가 마음에 든다.

 

⚙️ styled-components는 어떻게 적용되는것인가

기본적인 사용법은 공식 문서를 참고하면 쉽게 따라할 수 있기 때문에 굳이 블로그에 정리할 필요없는 없는 것 같아 styled-components는 어떻게 작동하는지 간단하게 알아보고자 한다. 참고로, 아래 설명글에서 styled.(*)에서 (*) tagged literal 함수는 factory로 명칭하도록 하겠다.

Import styled-components

앱에서 처음 라이브러리를 불러오면 styled.(*)로 만들어진 모든 컴포넌트의 개수를 세기 위해 내부적으로 counter이라는 변수가 만들어진다.

styled.(*)호출

const Button = styled.button`
  font-size: ${({ sizeValue }) => sizeValue + 'px'};
  color: coral; 
  padding: 0.25rem 1rem; 
  border: solid 2px coral; 
  border-radius: 3px;
  margin: 0.5rem;
  &:hover {
    background-color: bisque;
  }
`;

 

위 코드와 같이 styled.(*)로 새로운 컴포넌트를 만들면 counter변수가 증가하고 componentId라는 내부 식별자 값이 생성된다. coumponentId 값은 아래와 같이 MurmurHash 알고리즘을 통해 고유한 해시넘버 숫자를 알파벳으로 바꾸어 생성한다.

 

counter++;
const componentId = 'sc-' + hash('sc' + counter);

 

componentId가 만들어지면, styled-components는 HTML <style> element를 <head>에 주입한다. 그리고 주석으로 방금 생성한 commentId를 표시한다. (만약 해당 최초로 정의된 styled-components라면 <style> element를 주입하고, 그것이 아니라면 이미 주입된 <style>엘리먼트에 commentId주석만 추가된다.)

 

<style data-styled-components>
  /* sc-component-id: sc-bdVaJa */
</style>

 

컴포넌트가 만들어지면, 만들어진 해당 컴포넌트는 factory(예시 코드의 경우 button)의 target이 되고, componentId는 static 필드에 저장된다.

 

StyledComponent.componentId = componentId;
StyledComponent.target = TargetComponent;

 

factory를 이용해서 수많은 컴포넌트를 정의하고 사용하지 않더라도, 위에서 보다시피 styled component를 만드는 것만으로는 퍼포먼스적으로 무리를 주지는 않는다. 그저 componentId를 주석으로 가진 <style> element를 생성할 뿐이다.

또, 중요한 점으로 styled-components의 factory에서 만들어진 모든 컴포넌트들은 숨겨진 BaseStyledComponent라는 클래스를 상속받는다. BaseStyledComponent는 여러 컴포넌트의 라이프사이클 메소드를 실행시킨다.

 

componentWillMount()

위에서 styled factory로 작성한 Button컴포넌트를 페이지에 렌더링해보자

ReactDOM.render(
  <Button sizeValue={24}>I'm a button</Button>,
  document.getElementById('root')
);

BaseStyledComponentcomponentWillMount()라이프 사이클 메소드는 아래의 액션들을 실행시킨다.

  1. Tagged template을 통해 css string를 산출한다. 위에서 styled-components로 정의한 Button 컴포넌트에 sizeValue={24} props를 주고 렌더링한다면, 아래와 같은 css string이 산출된다. 이렇게 산출된 style을 설명을 위해 evaluated Styles라고 하겠다.
     <Button sizeValue={24}>I'm a button</Button>
     font-size: 24px;
     color: coral; 
     padding: 0.25rem 1rem; 
     border: solid 2px coral; 
     border-radius: 3px;
     margin: 0.5rem;
     &:hover {
     background-color: bisque;
     }

     

  2. 아까 만든 componentId와 위의 tagged template을 실행시키고 산출된 evaluated Styles을 MurmurHash 알고리즘을 통해 CSS 클레스 네임을 만든다.
     const className = hash(componentId + evaluatedStyles);

    Button 인스턴스에는 jsZVzX라는 className이 생성되었다. 이 클래스 네임은 컴포넌트의 generatedClassname라는 state에 저장된다.

  3.  

  4. stylis CSS preprocessor라는 CSS 전처리기를 통해 아까 생성한 클래스 네임과 evaluatedStyles을 합쳐 유효한 CSS를 만들어준다.
     .jsZVzX {
     font-size: 24px;
     color: coral;
     padding: 0.25rem 1rem;
     border: solid 2px coral;
     border-radius: 3px;
     margin: 0.5rem;
     }
     .jsZVzX:hover{
     background-color: bisque;
     }

     

  5. 위에서 생성된 CSS를 <style> element 안의 주석 바로 밑에 CSS string을 page에 넣어준다. componentId (.sc-bdVaJa)는 내부에 어떠한 룰도 없이 함께 코드에 주입된다.
     <style data-styled-components>
     /* sc-component-id: sc-bdVaJa */
     .sc-bdVaJa {} .jsZVzX{font-size:24px;color:coral; ... }  
     .jsZVzX:hover{background-color:bisque;}
     </style>

 

render()

css를 만드는 작업이 끝나면 className과 일치하는 컴포넌트를 생성하여 렌더링한다.

const TargetComponent = this.constructor.target; // In our case just 'button' string.
const componentId = this.constructor.componentId;
const generatedClassName = this.state.generatedClassName;

return ( 
  <TargetComponent 
    {...this.props} 
    className={this.props.className + ' ' + componentId + ' ' + generatedClassName}
  />
);

 

결론적으로, styled-components는 3개의 class name을 가지고 있는 셈이다.

  1. this.props.classname - 부모 컴포넌트로 부터 전달되는 class name으로 optional이다.
  2. componentId - 컴포넌트 인스턴스가 아닌 컴포넌트를 위한 유니크한 식별자. 이 클래스는 어떠한 CSS rule도 가지지 않는다. nesting selector를 사용할 때 다른 컴포넌트를 참조하기위해 사용된다.
  3. generatedClassName - 정의된 CSS rule을 가지며, 모든 컴포넌트 인스턴스가 개별로 가지고 있는 유니크한 식별자이다.

 

최종적으로 아래와 같은 클래스 네임을 가진 HTML element가 렌더링된다.

<button class="sc-bdVaJa jsZVzX">I'm a button</button>

 

👏 쓰고나니까 어때

  • 별도의 라이브러리나 중복되지 않게 클래스 이름 짓기 규칙 없이도 클래스 이름 겹치는거 신경안써도 되서 좋음
  • 리액트 코드와 겹치면 코드가 자칫 길어질 수도 있지만 컴포넌트를 잘 분리한다면 이런 경우는 거의 없을 것 같다. 특히나 presentational component와 container component를 분리하는 구조를 사용하고 컴포넌트도 잘 쪼갠다면 우려하는 경우는 더 없어질것 같음
  • 컴포넌트 props에 따라 클래스를 주는게 아니라 조건 스타일을 바로 주는게 간단하고 보기에도 깔끔하다. 이전에는 className에 덕지덕지 삼항연산자를 붙이고 그랬는데 그렇게하지 않아도 되서 좋다.(흠 근데 이건 다른사람이 코드를 이해하는데 있어서는 클래스네임으로 따로 구분하는 것이 더 나을 수도 있겠다는 생각이 든다.) styled-components 뿐만 아니라 CSS in JS 라이브러리들이 그렇듯 한 파일에서 JS와 코드를 공유할 수 있다는게 정말 큰 장점인것 같다.
  • 이번에 개인 레주메에 다크모드를 적용하면서 느꼈는데, styled-component에서 자체적으로 가지고있는 themeProvider 같은 컨텍스트 프로바이더도 잘 활용하면, 오직 스타일을 위한 props를 하위 컴포넌트에게 계속 넘겨주거나 컴포넌트마다 useContext()를 사용할 필요없이 모든 하위 컴포넌트들에서 바로 background: ${props => props.theme.color} 이런식으로 theme을 props로 받아 사용할 수 있어서 좋았다.
  • styled로 만드는 컴포넌트 이름을 따로 정해줘야하는데 StyledComponent 이런식으로 지으려다가 검색해보니 ComponentBlock 이런식으로 많이 짓는것 같아서 나도 ComponentBlock으로 줬다. 근데 별로 마음에 안든다(??)
  • styled-components는 컴포넌트를 'styled component(말그대로 styled.(*)와 같은 styled component)'로 한번 더 감싸줘야해서 보기에는 styled jsx가 더 깔끔하긴 한 것 같다.
  • react-native에서도 거의 비슷하게 사용할 수 있다고하니 react-native를 이용해서 처음 앱 개발을 시도해볼 때 러닝커브에 도움이 될 것 같다.
  • 결론은 styled-components던 styled jsx이던간에 CSS in JS 방식이 개인적으로는 개발효율을 높이는데에 좋았고 이정도면 안쓸 이유가 없다고 생각한다.

 

🍯 tip

  • 만약 vscode를 사용한다면, vscode-styled-components라는 extension을 사용하면 템플릿 리터럴 안에 쓰여진 css코드를 보기 좋게 하이라이팅해줘서 개발하는데 도움이 된다. 다른 에디터를 사용중이라면, 공식 docs의 Tooling-Syntax Highlighting 메뉴에서 에디터별로 플러그인을 추천해주고 있으니 참고하면 되겠다.
  • babel-plugin-styled-components을 사용하면 css 디버깅을 할때 className이 해시되기 전의 값으로 보여줘서 설치하면 디버깅하는데 도움이 된다. 이 또한 자세한 내용은 docs에 잘 정리되어있으니 참고하면 됨.

 

참고자료

https://medium.com/styled-components/how-styled-components-works-618a69970421

https://styled-components.com/docs/basics#motivation