React/타입스크립트로 배우는 리액트(React.js) : 기초부터 최신 기술까지

섹션 8. 컴포넌트 스타일링 (1)

favor 2026. 4. 16. 17:49

인라인 스타일

인라인 스타일 : JSX 요소의 스타일 속성을 사용해, 스타일을 객체 값으로 직접 지정하는 방식

// App.tsx

export default function App() {
  return (
    <>
      <h1 style={{ color: "red", fontSize: "50px" }}>Inline Style</h1>
    </>
  );
}

 

* 속성은 카멜케이스로 입력

 

변수에 스타일을 할당하고, 해당 변수를 바인딩하는 것 가능  

// App.tsx

export default function App() {

  const h1Style = { color: "red", fontSize: "50px" }; 

  return (
    <>
      <h1 style={h1Style}>Inline Style</h1>
    </>
  );
}

 

컴포넌트에 props로 전달 가능 

// App.tsx

import Inline from "./components/Inline";

export default function App() {

  const h1Style = { color: "red", fontSize: "50px" }; 

  return (
    <>
      <Inline h1Style={h1Style} />
    </>
  );
}
// Inline.tsx

export default function Inline({
  h1Style,
}: {
  h1Style: { color: string; fontSize: string };
}) {
  return (
    <>
      <h1 style={h1Style}>Inline Component</h1>
    </>
  );
}

 

삼항 연산자 사용 가능

// App.tsx

import Inline from "./components/Inline";

export default function App() {

  const isLoggedIn = false;
  const h1Style = { color: isLoggedIn ? "blue" : "red", fontSize: "50px" }; 

  return (
    <>
      <Inline h1Style={h1Style} />
    </>
  );
}

 

* && 연산자는 사용 불가능 

 

글로벌 스타일 

글로벌 스타일 : 별도의 css 파일을 만들어 스타일을 작성하는 방식

ㄴ 우리가 일반적으로 알고 있는 외부 스타일링 방식과 유사 

// App.tsx

import "./App.css";

export default function App() {
  return (
    <>
      <h1>App Component</h1>
    </>
  );
}
/* App.css */

h1 {
  color: red;
  text-decoration: underline;
  font-size: 50px;
}

 

* React에서 import로 불러온 CSS 파일은 모든 컴포넌트 트리에 영향을 줌 

/* App.css */

h1 {
  color: red;
  text-decoration: underline;
  font-size: 50px;
}
// App.tsx

import "./App.css";
import Global from "./components/Global";

export default function App() {
  return (
    <>
      <h1>App Component</h1>
      <Global />
    </>
  );
}
// Global.tsx

export default function Global () {
  return (
    <>
      <h1>Global Component</h1>
    </>
  );
}

^ 실제로 Global 컴포넌트에서 App.css를 import 하지 않았음에도 DOM 트리 내에서 동일한 스타일을 공유하게 되어 h1 요소에 스타일이 적용됨 

 

* 글로벌 스타일을 적용할 때에는 import 경로를 main.tsx 파일에 몰아서 작성하는 것이 좋음

ㄴ 개별적으로 컴포넌트에 import 하는 것보다 관리하기 편함 

 

CSS 모듈

CSS 모듈 : 확장자가 'module.css'로 끝나는 파일에 CSS를 작성하는 방식 

ㄴ 특정 컴포넌트에서만 스타일이 공유되도록 하기 위해 사용

ex) App.module.css

/* App.module.css */

.title {
  color: red;
  text-decoration: underline;
  font-size: 50px;
}
// App.tsx

import styles from "./App.module.css";
import Global from "./components/Global";

export default function App() {
  return (
    <>
      <h1 className={styles.title}>CSS Module</h1>
      <Global />
    </>
  );
}

 

* CSS 파일에는 클래스 선택자만을 사용해서 스타일을 지정해야 함 

ㄴ 태그 선택자, 아이디 선택자와 같은 것들을 사용하는 것은 원칙에 위배 

--> 글로벌 스타일처럼 동작

 

classnames

CSS 모듈 방식은 CSS 모듈 객체를 매번 활용해야 하기 때문에 코드가 길어질 수 있다는 단점 O

--> 이러한 단점을 해결하기 위해서 외부 패키지 중 classnames 패키지를 많이 사용 

 

classnames : CSS 클래스 이름을 동적으로 조합하고 관리할 수 있도록 도와주는 유틸리티 

npm install classnames 명령어를 사용하여 다운로드 

 

/* App.module.css */

.btn {
  width: 100px;
  height: 50px;
  border: none;
  background-color: gray;
  color: white;
  font-size: 14px;
  border-radius: 10px;
  margin: 0 10px;
}

.is-active {
  background-color: blue;
}
// App.tsx

import styles from "./App.module.css";

export default function App() {
  return (
    <>
      <button className={`${styles.btn} ${styles["is-active"]}`}>버튼</button>
    </>
  );
}

^ 하나의 태그에 2개의 스타일을 적용하기 위한 방법 

--> 코드가 길어지고 가독성이 떨어짐 

 

classnames 패키지 사용

/* App.module.css */

.btn {
  width: 100px;
  height: 50px;
  border: none;
  background-color: gray;
  color: white;
  font-size: 14px;
  border-radius: 10px;
  margin: 0 10px;
}

.is-active {
  background-color: blue;
}
// App.tsx

import classNames from "classnames/bind";
import styles from "./App.module.css";

export default function App() {

  const cx = classNames.bind(styles);

  return (
    <>
      <button className={`${styles.btn} ${styles["is-active"]}`}>버튼</button>
      <button className={cx("btn", "is-active")}>버튼</button>
    </>
  );
}

^ cx가 styles 객체 안에서 값을 매핑, 바인딩함

 

// App.tsx

import classNames from "classnames/bind";
import styles from "./App.module.css";

export default function App() {

  const cx = classNames.bind(styles);
  const isLoggedIn = false;

  return (
    <>
      <button className={`${styles.btn} ${isLoggedIn ? styles["is-active"] : ""}`}>버튼</button>
      <button className={cx("btn", { "is-active": isLoggedIn })}>버튼</button>
    </>
  );
}

^ 상태 값의 변화에 따라 클래스명을 동적으로 제어할 때, classnames/bind를 활용하면 복잡한 조건부 로직을 객체 구조로 간결하게 표현 가능 

 

CSS-IN-JS 소개 

전통적인 웹 개발 방식에서는 HTML, CSS, JavaScript를 각각 분리하여 개발하는 것을 추구 

= 관심사 분리라는 소프트웨어 설계 원칙에 따른 것으로, 각 기술의 역할을 명확히 구분하여 유지보수성을 높이고자 함

그러나 웹이 발전하면서 컴포넌트 기반 개발이 보편화되었고, 전통적인 방식이 몇 가지 문제를 일으킴 

 

  1. 전역 스코프 문제
    • 기존의 CSS는 전역 스코프로 동작하기 때문에 클래스명 충돌이 빈번하게 발생
  2. 상태 관리의 어려움 
    • UI 상태는 JS로 작성하는데, 스타일은 CSS 파일에서 작성하기 때문에 동적인 상태를 추가하는 게 번거롭고 어려움
  3. 유지보수의 복잡성
    • 프로젝트의 규모가 커질수록, 어떤 CSS가 어떤 컴포넌트에 영향을 주는지 찾기 어려움
    • 사용하지 않는 CSS도 완전히 제거하기 어려움 

 

위의 문제를 해결하고자 새롭게 등장한 스타일링 방식이 CSS-IN-JS 방식 

 

CSS-IN-JS : 자바스크립트 파일 안에서 CSS 스타일을 정의하고 적용하는 접근 방식 

ㄴ 자바스크립트로 개발하는 컴포넌트 기반 방식에 매우 적합, 동적 스타일링을 쉽게 구현 가능 

 

CSS-IN-JS 라이브러리 중 React에서 자주 사용하는 세 가지 라이브러리 

  • Styled-Component
  • Emotion
  • Vanilla Extract

 

styled-components (기본)

Styled-Components : 스타일 속성을 적용해서 하나의 완성된 컴포넌트 태그를 만들어 사용하는 방식 

ㄴ 외부 라이브러리이기 때문에 npm install styled-components 명령어로 패키지를 설치해야만 사용 가능 

 

기본 문법

const 컴포넌트이름 = styled.[태그명]`
  스타일 속성;
`;

 

// App.tsx

import styled from "styled-components";

const Title = styled.h1`
  color: red;
  text-decoration: underline;
`;

const Wrapper = styled.section`
  padding: 2rem;
  border: 1px solid red;
`;

export default function App() {
  return (
    <>
      <Wrapper>
        <Title>Hello, ReactJS!</Title>
      </Wrapper>
    </>
  );
}

 

styled-components (props)

// App.tsx

import styled from "styled-components";

const Title = styled.h1<{ $color: string; $decoration: string}>`
  color: ${(props) => props.$color};
  text-decoration: ${(props) => props.$decoration};
`;

const Wrapper = styled.section`
  padding: 2rem;
  border: 1px solid red;
`;

export default function App() {
  return (
    <>
      <Wrapper>
        <Title $color="green" $decoration="line-through">Hello, ReactJS!</Title>
      </Wrapper>
    </>
  );
}

 

styled-components (확장)

기존에 생성한 스타일 컴포넌트에서 일부를 바꾼 새 컴포넌트가 필요한 경우에는 확장을 사용

새로 만들어진 컴포넌트는 기존 컴포넌트의 스타일을 모두 상속받음

* 기존 컴포넌트가 props를 받고 있을 때에는 기존 컴포넌트도 props를 넘겨받아야 함 

// App.tsx

import styled from "styled-components";

const Title = styled.h1<{ $color: string; $decoration: string}>`
  color: ${(props) => props.$color};
  text-decoration: ${(props) => props.$decoration};
`;

const BigTitle = styled(Title)`
  font-size: 50px;
`

const Wrapper = styled.section`
  padding: 2rem;
  border: 1px solid red;
`;

const BlueBorderWrapper = styled(Wrapper)`
  border-color: blue;
`

export default function App() {
  return (
    <>
      <BlueBorderWrapper>
        <Title $color="green" $decoration="line-through">Hello, ReactJS!</Title>
        <BigTitle $color="blue" $decoration="underline">Hello, Big Title!</BigTitle>
      </BlueBorderWrapper>
    </>
  );
}

 

styled-components (as, keyframes)

styled-components는 as 속성을 제공

as : 스타일이 입혀진 컴포넌트의 디자인(CSS)은 그대로 유지하면서, 렌더링되는 HTML 태그만 다른 것으로 교체할 때 사용하는 속성

// App.tsx

import styled from "styled-components";

const Title = styled.h1<{ $color: string; $decoration: string}>`
  color: ${(props) => props.$color};
  text-decoration: ${(props) => props.$decoration};
`;

const BigTitle = styled(Title)`
  font-size: 50px;
`

const Wrapper = styled.section`
  padding: 2rem;
  border: 1px solid red;
`;

const BlueBorderWrapper = styled(Wrapper)`
  border-color: blue;
`

export default function App() {
  return (
    <>
      <BlueBorderWrapper>
        <Title $color="green" $decoration="line-through" as="h4">Hello, ReactJS!</Title>
        <BigTitle $color="blue" $decoration="underline">Hello, Big Title!</BigTitle>
      </BlueBorderWrapper>
    </>
  );
}

^ as 속성을 활용해 기존 Title 컴포넌트의 children으로 h4 태그로 출력 

 

styled-components는 keyframes 속성을 제공

keyframes : 애니메이션을 하나의 변수(객체)로 저장해두고, 여러 스타일 컴포넌트에서 가져다 쓸 수 있도록 해주는 속성 

// App.tsx

import styled, { keyframes } from "styled-components";

const fadeIn = keyframes`
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
`;

const Title = styled.h1<{ $color: string; $decoration: string}>`
  color: ${(props) => props.$color};
  text-decoration: ${(props) => props.$decoration};
  animation: ${fadeIn} 2s ease-in;
`;

const BigTitle = styled(Title)`
  font-size: 50px;
`

const Wrapper = styled.section`
  padding: 2rem;
  border: 1px solid red;
`;

const BlueBorderWrapper = styled(Wrapper)`
  border-color: blue;
`

export default function App() {
  return (
    <>
      <BlueBorderWrapper>
        <Title $color="green" $decoration="line-through" as="h4">Hello, ReactJS!</Title>
        <BigTitle $color="blue" $decoration="underline">Hello, Big Title!</BigTitle>
      </BlueBorderWrapper>
    </>
  );
}

^ keyframes 속성을 활용해 컴포넌트에 애니메이션 적용

 

styled-components (CSS 헬퍼, 믹스인)

CSS 헬퍼 : 자주 사용하는 코드 블럭을 하나의 변수(코드 블록)로 묶어서 관리할 수 있게 해주는 함수

ㄴ 조건부로 스타일을 적용하는 용도로 활용 가능 

// App.tsx

import styled, { keyframes, css } from "styled-components";

const boxShadowMixin = css`
  box-shadow: 0px 10px 10px rgba(0, 0, 0, 0.5);
`;

const fadeIn = keyframes`
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
`;

const Title = styled.h1<{ $color: string; $decoration: string}>`
  color: ${(props) => props.$color};
  text-decoration: ${(props) => props.$decoration};
  animation: ${fadeIn} 2s ease-in;
`;

const BigTitle = styled(Title)`
  font-size: 50px;
`

const Wrapper = styled.section`
  padding: 2rem;
  border: 1px solid red;
`;

const BlueBorderWrapper = styled(Wrapper)<{ $shadow: boolean }>`
  border-color: blue;
  ${(props) => props.$shadow && boxShadowMixin}; // 조건부 스타일 지정
`

export default function App() {
  return (
    <>
      <BlueBorderWrapper $shadow> {/* shadow를 쓰면 shadow가 true로 들어감 */}
        <Title $color="green" $decoration="line-through" as="h4">Hello, ReactJS!</Title>
        <BigTitle $color="blue" $decoration="underline">Hello, Big Title!</BigTitle>
      </BlueBorderWrapper>
    </>
  );
}

 

styled-components (전역)

styled-components는 일관된 디자인 시스템을 적용하기 위해 테마 기능을 제공

테마 기능 : 특정 스타일이 정의된 변수를 컴포넌트 트리 전체에 공급하여 이를 스타일 컴포넌트에서 참조할 수 있게 하는 방식

// main.tsx

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";

import App from "./App.tsx";
import { ThemeProvider } from "styled-components";

const theme = {
  colors: {
    primary: "red",
    secondary: "gray",
  },
  fontSize: {
    normal: "16px",
    large: "20px",
  },
};

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <ThemeProvider theme={theme}>
      <App />
    </ThemeProvider>
  </StrictMode>
);
// App.tsx

import styled, { keyframes, css } from "styled-components";

const boxShadowMixin = css`
  margin: 20px;
  box-shadow: 0px 10px 10px rgba(0, 0, 0, 0.5);
`;
const fadeIn = keyframes`
  from{
    opacity: 0;
  }
  to{
    opacity: 1;
  }
`;

const Title = styled.h1<{ $color: string; $decoration: string }>`
  color: ${(props) => props.$color};
  text-decoration: ${(props) => props.$decoration};
  animation: ${fadeIn} 2s ease-in;
`;

const BigTitle = styled(Title)`
  font-size: ${(props) => props.theme.fontSize.large};
`;

const Wrapper = styled.section`
  padding: 2rem;
  border: 1px solid red;
`;

const BlueBorderWrapper = styled(Wrapper)<{ $shadow: boolean }>`
  border-color: ${(props) => props.theme.colors.primary};
  ${(props) => props.$shadow && boxShadowMixin}
`;

export default function App() {
  return (
    <>
      <BlueBorderWrapper $shadow>
        <Title $color="#0000ff" $decoration="underline" as="p">
          Hello, ReactJS!
        </Title>
        <BigTitle $color="#0000ff" $decoration="underline">
          Hello, Big Title!
        </BigTitle>
      </BlueBorderWrapper>
    </>
  );
}

 

styled-components (팁)

앰퍼센트(&)를 활용한 선택자 지정 방식으로 여러 스타일 적용 가능

앰퍼센트 : 현재 컴포넌트를 가리키는 지시자

// App.tsx

import styled, { keyframes, css } from "styled-components";

const boxShadowMixin = css`
  box-shadow: 0px 10px 10px rgba(0, 0, 0, 0.5);
`;

const fadeIn = keyframes`
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
`;

const Title = styled.h1<{ $color: string; $decoration: string}>`
  color: ${(props) => props.$color};
  text-decoration: ${(props) => props.$decoration};
  animation: ${fadeIn} 2s ease-in;
  &:hover {
    color: pink;
  }
`;

const BigTitle = styled(Title)`
  font-size: 50px;
`

const Wrapper = styled.section`
  padding: 2rem;
  border: 1px solid red;
`;

const BlueBorderWrapper = styled(Wrapper)<{ $shadow: boolean }>`
  border-color: blue;
  ${(props) => props.$shadow && boxShadowMixin}; 
`

export default function App() {
  return (
    <>
      <BlueBorderWrapper $shadow> {/* shadow를 쓰면 shadow가 true로 들어감 */}
        <Title $color="green" $decoration="line-through" as="h4">Hello, ReactJS!</Title>
        <BigTitle $color="blue" $decoration="underline">Hello, Big Title!</BigTitle>
      </BlueBorderWrapper>
    </>
  );
}

 

  • $ 기호가 붙은 속성 ($props)
    • 오직 스타일 정의를 위한 용도로만 사용
    • CSS 내부 처리용
    • 실제 HTML 태그로는 전달되지 않고 스타일 컴포넌트 내부에서만 소비된 후 사라짐
  • $ 기호가 붙지 않은 속성
    • HTML 표준 속성(ex. id, href, title, onClick)이나, 하위 컴포넌트까지 전달되어야 하는 속성들 

 

// App.tsx

import styled, { keyframes, css } from "styled-components";

const boxShadowMixin = css`
  box-shadow: 0px 10px 10px rgba(0, 0, 0, 0.5);
`;

const fadeIn = keyframes`
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
`;

const Title = styled.h1<{ $color: string; $decoration: string}>`
  color: ${(props) => props.$color};
  text-decoration: ${(props) => props.$decoration};
  animation: ${fadeIn} 2s ease-in;
  &:hover {
    color: pink;
  }
`;

const BigTitle = styled(Title)`
  font-size: 50px;
`

const Wrapper = styled.section`
  padding: 2rem;
  border: 1px solid red;
`;

const BlueBorderWrapper = styled(Wrapper)<{ $shadow: boolean }>`
  border-color: blue;
  ${(props) => props.$shadow && boxShadowMixin}; 
`

export default function App() {
  return (
    <>
      <BlueBorderWrapper $shadow> {/* shadow를 쓰면 shadow가 true로 들어감 */}
        <Title $color="green" $decoration="line-through" as="h4" onClick={() => alert('hello!')}>Hello, ReactJS!</Title>
        <BigTitle $color="blue" $decoration="underline">Hello, Big Title!</BigTitle>
      </BlueBorderWrapper>
    </>
  );
}

^ Title 컴포넌트의 속성 중 $ 기호가 붙은 것과 붙지 않은 것이 있음