[뭐라도 개발해보자] React를 배워보자

hjboom·2025년 4월 27일

뭐라도 개발하기

목록 보기
1/3
post-thumbnail

정말 뭐라도 개발하고 조금이라도 성장하고 싶어서

왜?
프론트 기술을 하나도 몰라서 가끔 만들어보고 싶은 것들을 구현하지 못하는 경우가 많았다. 그래서 그냥 직접 배워보기로 했다.

뭐를?
기술 스택은 많이 사용하는 React로 정했다. 고른 이유는 사람들이 제일 많이 쓰는 것 같아서 골라봤다.

어떻게?
주변 개발자들에게 물어보니 초반에 감잡는 것에는 클론 코딩만한게 없다 하기에 한 번 배워보고자 영상을 선택했다. 이후에는 요즘 만들어보고 싶은 간단한 화면이 있어 실제 서버와 연결해서 한 번 화면 구성까지 해볼 것이다.

https://www.youtube.com/watch?v=ymGB1lqP1CM&t=1026s

어제부터 시작했고, tailwind css vsc extension이 제대로 적용 안되어서 그거 설정하느냐고 조금 버벅였다.

아래는 그 다음내용 부터 이해되지 않거나 한 부분을 공부하며 끄적인 내용이다.


Button을 만들어보자

Cva란?

import { cva, VariantProps } from "class-variance-authority"
import { ComponentProps } from "react"

const buttonStyles = cva(["transition-colors"], {
  variants: {
    variant: {
      default: ["bg-secondary", "hover:bg-secondary-hover"],
        ghost: ["hover:bg-gray-100"]
    },
    size: {
      default: [" rounded", "p-2"],
      icon: [
        "rounded-full",
        "w-10",
        "h-10",
        "flex",
        "items-center",
        "justify-center",
        "p-2.5",
      ]
    }
  },
  defaultVariants: {
    variant: "default",
    size: "default"
  }
})

class-variance-authority라는 library를 import 해보았다. cva는 다양한 variant를 가진 className을 깔끔하게 생성해주는 함수라고 한다.

cva함수는 첫번째 인자로 base class list를 받는다. 항상 적용될 기본 class들을 작성하는 곳이다. 위의 함수에는 기본 class로 transition-colors가 적용되어 있는데, 이는 색이 자연스럽게 바뀌는 효과를 적용한 것이다. 두번째 인자로는 variant를 종류별로 나누고 나중에 사용할 때 사용하고 싶은 스타일을 골라서 사용하게 한다.

variant란?

찾아보니 종류를 의미하는 것 같다. 그리고 특히 스타일의 종류를 의미하는 것 같다. 위에서는 variant로 두 가지를 정의했다. 기본 스타일과 ghost 스타일. default 스타일에는 두 가지 스타일을 동시에 적용하고 싶어서 배열로 인자를 넘긴 것이다. 배열 안에 들어가는 것은 className이다. “bg-secondary”라는 className을 가진 class를 tailwind에서 만들어 두었으므로 이를 적용하겠다고 선언하는 것이다.

근데 나는 색을 이렇게 저장했다.

colors: {
        secondary: {
          DEFAULT: colors.neutral[200],
          hover: colors.neutral[300],
          border: colors.neutral[400],
          text: colors.neutral[500],
          dark: colors.neutral[800],
          ["dark-hover"]: colors.neutral[900]
        }
      }

찾아보니, 색을 tailwind에 등록하게 되면 tailwind가 알아서 css class를 여러개 생성한다고 한다. 그 중 하나가 bg-secondary이기에 내가 그걸 가져다 쓸 수 있는듯. 그리고 secondary도 여러개로 정의할 수 있어서 “bg-secondary-hover”와 같이 표현할 수 있는듯 하다.

button component 만들기

type ButtonProps = VariantProps<typeof buttonStyles> & ComponentProps<"button">

export function Button({ variant, size, ...props }: ButtonProps) {
  return <button {...props} className={buttonStyles({ variant, size })} />
}

이걸보고 지금까지 무엇을 한지 깨달았다. Button function을 만들어서 추상화를 통해 여러 곳에서 사용될 component를 만들고 싶었던 것이다.

저렇게 type을 정하는 것이 처음이라 조금 신기했다. VaraintProps는 cva library에서 지원하는 함수로 cva로 정의한 variant 옵션들을 타입으로 만들어주는 타입 헬퍼이다.

{
  variant?: "default" | "ghost",
  size?: "default" | "icon"
}

이런식으로 만들어준다고 생각하면 좋다.

ComponentProps는 원래 html tag가 가질 수 있는 props type을 반환하는 함수이다.

이렇게 두 타입을 섞어서 타입으로 지정하고, 추후에 다른 variant나 size가 추가 되어도 type 수정을 하지 않게 추상화를 하는 것이다. 이제 저렇게 타입을 선언하면 우린 type을 수정할 일이 거의 없어진다.

그리고 이런 component는 실제로 아래와 같이 적용이 되게 된다.

<button class="transition-colors bg-secondary hover:bg-secondary-hover p-2">
  <Menu />
</button>

결과

이제 만들어진 button은 적용해보겠다.

export function PageHeader() {
  return <div className="flex gap-10 lg:gap-20 justify-between">
    <div className="flex gap-4 items-center flex-shrink-0">
      <Button>
        <Menu />
      </Button>
      
      ...
      
}

이제 마우스를 올리면 살짝 흐려지는 왼쪽 메뉴 버튼을 만들 수 있게 됐다.

그럼 variant들을 설정해보겠다.

export function PageHeader() {
  return <div className="flex gap-10 lg:gap-20 justify-between">
    <div className="flex gap-4 items-center flex-shrink-0">
      <Button variant="ghost" size="icon">
        <Menu />
      </Button>
      
      ...
      
}

하나의 컴포넌트로 여러 스타일을 적용할 수 있게 되었다. 이렇게 하지 않으면 버튼 종류 하나하나마다 모두 컴포넌트를 만들어야 하기 때문에 코드의 재활용성이 매우 떨어질 것이다.

근데 className을 외부에서 전달하고 싶으면?

component는 현재 무조건 Button component가 필요로하는 props만 받을 수 있게 되어 있다. 그런데 우리가 padding 값을 주고 싶어서 이를 인자로 넘겨주면 어떻게 될까?

import { Menu } from "lucide-react"
import logo from "../assets/logo.png"
import { Button } from "../components/Button"

export function PageHeader() {
  return <div className="flex gap-10 lg:gap-20 justify-between">
    <div className="flex gap-4 items-center flex-shrink-0">
      <Button variant="ghost" size="icon" className="p-10">
        <Menu />
      </Button>
			
			...
			
  </div>
}

해보면 알겠지만 넘겨준 className은 아예 적용이 되지 않는다. component 함수에서 이를 return할 때 새롭게 만들어 덮어쓰는 구조이기 때문에 그렇다. 그렇다면 이렇게 적용해볼까?

export function Button({ variant, size, className, ...props }: ButtonProps) {
  return (
    <button {...props} className={buttonStyles({ variant, size, className })} />
  )
}

입력으로 들어오는 className또한 포함을 시켜 return을 해주는 것이다. 이렇게 하면 제대로 적용이 된다. 하지만 이렇게 사용하지 않는다고 한다. 그 이유는 tailwind css와 충돌이 발생할 수 있기 때문이다. css가 적용되는 순서에 따라 어느게 적용될 지 언제나 보장할 수 없기 때문이라고. 그래서 만약에 className의 인자로 넣은 값이 component의 style과 겹쳐서 여러 동일한 범주의 className이 동시에 들어가게 되면(예를 들어 p-2, p-10이 같이 들어감) 어느 값이 적용될 지 알 수 없다고 한다.

이를 위해 twMerger라는 함수를 적용해서 항상 나중에 나오는 것이 적용되도록 할 수 있다.

export function Button({ variant, size, className, ...props }: ButtonProps) {
  return (
    <button 
    {...props} 
    className={twMerge(className, buttonStyles({ variant, size }))} />
  )
}

화면 구성

import { Bell, Menu, Mic, Search, Upload, User } from "lucide-react"
import logo from "../assets/logo.png"
import { Button } from "../components/Button"

export function PageHeader() {
  return <div className="flex gap-10 lg:gap-20 justify-between pt-2 mb-6 mx-4">
    <div className="flex gap-4 items-center flex-shrink-0">
      <Button variant="ghost" size="icon">
        <Menu />
      </Button>
      <a href="/">
        <img src={logo} className="h-6" />
      </a>
    </div>
    <form className="flex gap-4 flex-grow justify-center">
      <div className="flex flex-grow max-w-[600px]">
        <input type="search" placeholder="Search" className="rounded-l-full border border-secondary-border shadow-inner shadow-secondary" />
        <Button>
          <Search />
        </Button>
      </div>
      <Button type="button" size="icon" className="flex-shrink-0">
        <Mic />
      </Button>
    </form>
    <div className="flex flex-shrink-0 md:gap-2">
      <Button size="icon" variant="ghost">
        <Upload />
      </Button>
      <Button size="icon" variant="ghost">
        <Bell />
      </Button>
      <Button size="icon" variant="ghost">
        <User />
      </Button>
    </div>
  </div>
}

그 뒤로는 특별한게 없었다. Button component의 강력한 모습을 보여주는 느낌. 궁금한 것은 저 많은 className을 어떻게 적용하여야 디자이너의 의도 및 사용성을 갖춘 화면이 나오는 지 궁금하다는 것과 className의 종류가 이렇게 많은데 어느게 적재적소에 맞는 지 아는지 정도? 오늘은 여기까지.

1개의 댓글

comment-user-thumbnail
2025년 4월 27일

tailwind를 사용하시게된 이유가 궁금합니다~

답글 달기