Edwin-Blog (2) Styled-components

Edwin·2023년 6월 3일
0

edwinblog

목록 보기
3/5
post-thumbnail

Edwin-Blog (2) Styled-components



이번 글에서는 타입스크립트 리액트에서 Css in TS를 통해 동적으로 스타일을 제어하는 방법에 대해서 정리하며, 직면했던 한 가지 문제를 소개하고자 한다.

yarn add

  yarn add styled-components
  yarn add @types/styled-components

타입스크립트는 JS의 슈퍼셋으로 런타임 단계에서 확인되는 에러를 컴파일 시에 제어함으로 코드 안정성을 끌어올린 프로그램 언어이다. 그러기에 기존에 타입에 대한 고민없이 사용했던 styled-components도 타입을 지정해주어야 문제없이 코드를 작성할 수 있다. 관련된 내용은 다른 포스트에 기록했으니, 해당 부분을 참고 바란다.

styled 함수에 대한 type 설정

컴포던트들을 스타일 단위로 구분하여 CSS 파일이 아니라, JS언어로 제어하는 라이브러리가 CSS in JS이다. 리액트에서 동적으로 제어하기 위해서는 조건문과 같은 true/false 값 또는 재사용성을 위해서 문자열이나, 숫자열을 props로 내려주는 행위를 통해서 제어가 가능하다.

.ilbuni {
	position: absolute;
	width: 40%;
    
    img {
      max-width: 100%;
      height: auto;
  	}
}

.ilbuni:nth-of-type(1) {
	width: 25%;
	left: 42% ;
	bottom: 40%;
	transform: translateZ(5px);
}

.ilbuni:nth-of-type(2) {
	width: 45%;
	right: 10%;
	bottom: 10%;
	transform: translateZ(10px);
}

.ilbuni:nth-of-type(3) {
	width: 50%;
	left: 5%;
	bottom: 3%;
	transform: translateZ(30px);
}

위의 코드는 1분코딩님의 강의 내용에서 나온 CSS 코드 부분이다. JS를 통해서 CSS를 다룬다는 것은 코드의 재사용성을 높이겠다는 것을 의미하기도 한다. 이를 기반으로 코드를 아래와 같이 확장성을 높인 코드를 만들었다.

export const PointEmmoji = styled.figure<InfoArr>`
  position: absolute;
  width: ${({ info }) => info[0]};
  left: ${({ info }) => info[1]};
  bottom: ${({ info }) => info[2]};
  transform: translateZ(${({ info }) => info[3]});

  img {
    max-width: 100%;
    height: auto;
  }
`

CSS 선택자에 의해서 기록한 코드와 바로 위의 TS로 기록된 코드를 보면 중복되는 부분이 존재한다. 최대한 중복되는 부분을 관리하고자, 리액트에서 props라 불리는 요소를 통해서 정보를 내려주고자 했으며, 관련된 내용의 Type을 InfoArr 제너릭을 통해서 정의해주었다.

export type InfoArr = {
  info:string[];
}

props로 내려줄 코드를 보면, 4개의 문자열이 묶인 string Type의 배열이 필요하다. 이를 위해서 타입을 string[] 지정해주었고, 이제 사용하는 컴포넌트에서 해당 Type으로 리소를 내려주면 된다.

import React from 'react'
import dinosaur from '../../assets/dinosaur.png'
import * as Card from './styled';


const Home3DCard: React.FC = () => {

  return (
      <Card.Layoyt>
    	...
    
        <Card.PointEmmoji
          info={["25%", "80%", "75%", "5px"]}
          children={<img src={dinosaur} alt='dinosaur' />} />
        <Card.PointEmmoji
          info={["35%", "75%", "25%", "10px"]}
          children={<img src={dinosaur} alt='dinosaur' />} />
        <Card.PointEmmoji
          info={["50%", "-10%", "20%", "13px"]}
          children={<img src={dinosaur} alt='dinosaur' />} />

		...
      </Card.Poster>
    </Card.Layoyt>
  )
}

export default Home3DCard

이전의 상황에서 props 각각의 내용을 따로따로 보내준 기억이 있는데, 이런 식으로 배열로 내려주는 부분은 처음해봤지만 문제가 역시 없었다. 순서대로 width, left, bottom, translateZ에 필요한 내용들을 기록해 주면 된다.

문제발생, Body 태그에 적용한 스타일에 관해서


  • 이미지 1 : 아이폰 13pro 가로모드

  • 이미지 2 : 아이폰 13pro 가로모드

이미지 1과 이미지 2의 차이는 뷰포트 100vw에서 발생된 문제이다. 프로젝트는 '/'에서만 노랑색 배경을 선언하고, 이외의 라우터에서는 흰배경을 적용하고 싶었다. 그래서 createGlobalStyled의 body {} 선택자를 통해서 적용한 부분을 Home.tsx 로 가져와 별도의 크기에 100vw를 설정하고, 색을 지정하였다.

그런데 발생된 문제는 아이폰13Pro 가로모드 시, 좌우측에 흰여백이 발생되었다. 100vw임에도 말이다. body {}에서는 모든 공간이 채워졌지만, Home.tsx에서 영억을 지정하고 선언하면 적용되지 않았다.

이를 위해서 내가 도입한 방법은 전역에서 Background 상태를 관리하고 이를 createGlobalStyled에 props 내려서 적용시키는 방법이었고, 일단 문제를 해결했다.

const App:React.FC =() => {
  const background = useRecoilValue(globalBackground)
  return (
    <>
    <GlobalStyled info={background}/>
      <Router />
    </>
  )
}

export default App

App.tsx에 GlobalStyled를 주입시켜서 동적으로 CSS를 제어하였다.

import { atom } from "recoil";
export const globalBackground = atom<string>({
  key: 'globalBackground',
  default: "#F6BB43",
});

처음의 초기값은 페이지에 처음 등장하는 라우터의 배경인 노란색(#F6BB43)을 선언하였다. 처음에는 투명색을 선언하고, Home.tsx에서 사이드이펙트(useEffect)를 통해서 제어하고자 했지만, 사이드이펙트는 컴포넌트가 마운트 된 이후에 동작함으로 반박자 늦은 사용자경험을 제공하기에, 초기값으로 '/' 경로의 배경색을 입력했다.

const Home: React.FC = () => {
  const [,setBackground] = useRecoilState(globalBackground)
  useEffect(() => {
    setBackground("#F6BB43")
    return () => {
      setBackground("#transparent")
    }
  },[setBackground])
  return (
    <>
      <Home3DCard/>
      <WelComeFiguer
        children={<img src={WelCome} alt='WelCome'/>}/>
      <ArrowFiguer 
        children={<img src={CardClick} alt='arrow'/>}/>
    </>
  )
}

export default Home

이후 클릭 이벤트를 통해서 다른 라우터로 이동했을 때에 전역에서 관리하는 상태관리의 상태를 변경하고자 사이드이펙트로 마운트가 해제되었을 때 색상이 변경되도록 설정하였다.

const Main: React.FC = () => {
  const [, setBackground] = useRecoilState(globalBackground)
  useEffect(() => {
    setBackground("#transparent")
  }, [setBackground])
  return (
    <div>Main
     ...
    </div>
  )
}

export default Main

그리고 이동된 라우터에서 사이드이팩트를 통해서, 해당 경로에서도 새로고침하더라도 초기값인 노란색이 아니라 #transparent를 유지하도록 하였다. 그러나 해당 방법은 생성한 라우터마다 기록해줘야 한다는 점에서 좋은 접근은 아니기에, 리팩토링하면서 해당 문제에 접근하려 한다.

정리하면, CSS in TS에서 동적으로 스타일을 제어할 수 있는데, 이를 위해서는 props로 내려줄 상태의 Type을 지정해 주면 된다는 것이었다. 또한 다시금 사이드 이펙트의 랜더링 시점을 고민해 보는 시간이 되기도 하였다.

선택 : 처음로딩과 라우터 새로고침의 제어

추후 변경된 부분은 처음로딩 때 배경이 흰색 => Home 라우터의 색으로 변경되도록 글로벌 스타일에 대한 전역 상태관리를 변경했고, useEffect제어는 Home에서만 설정하도록 변경하였다. 그러나 사이드이펙트 이기에 화면에 렌더링이 끝난 이후에 동작한다. 이런 부분에서 대응이 살짝 늦는 것을 볼 수 있다. 그런데 이 부분을 사이드 이펙트가 아니라 런타임시에, 즉 DOM이 그려질 때 동작하게 만들 수 있지 않을까?

const Home: React.FC = () => {
  const [,setBackground] = useRecoilState(globalBackground)
  setBackground("#F6BB43")
  useEffect(() => {
    return () => {
      setBackground("transparent")
    }
  },[setBackground])
  return (
    <>
      <Home3DCard/>
      <WelComeFiguer
        children={<img src={WelCome} alt='WelCome'/>}/>
      <ArrowFiguer 
        children={<img src={CardClick} alt='arrow'/>}/>
    </>
  )
}

setBackground("#F6BB43")의 위치를 변경함으로 개선했으며, 컴포넌트가 언마운트될(해제) 때에만 상태가 변경되도록 설정을 바꾸었다.

useLayoutEffect

또는 아래와 같이 선언하는 것도 가능하다.

useLayoutEffect(()=> {
    console.log("리렌더링?")
    setBackground("#F6BB43")
  },[setBackground])

useLayoutEffect는 실제 화면이 그려지기 전에 실행되고, useEffect는 실제 화면이 그려진 이후에 실행된다는 점에서 차이가 있다.

author. EDWIN
date. 23/06/03

profile
신학전공자의 개발자 도전기!!

0개의 댓글