본문 바로가기
  • 기록
React/(Udemy)React-The Complete Guide

[React] 리액트 컴포넌트 스타일링(세션 6): styled components, css모듈

by juserh 2022. 5. 28.
  • Conditional & Danamic Styles
  • Styled Components: third party library, 특정 범위에 대해서만 css 스타일 적용하기
  • CSS modules

기본 프로젝트


1. 동적 인라인 스타일 설정하기

먼저 기본 프로젝트에서는 input에 아무것도 입력하지 않고 Add Goal 버튼을 눌러도 아이템이 추가된다. 이것을 피하려면 스타일을 동적으로 설정해야 한다.

 

먼저 기존 input관련 컴포넌트 코드를 살펴보면, 아래와 같다.

import React, { useState } from "react";

import Button from "../../UI/Button/Button";
import "./CourseInput.css";

const CourseInput = (props) => {
  const [enteredValue, setEnteredValue] = useState("");

  const goalInputChangeHandler = (event) => {
    setEnteredValue(event.target.value);
  };

  const formSubmitHandler = (event) => {
    event.preventDefault();
    props.onAddGoal(enteredValue);
  };

  return (
    <form onSubmit={formSubmitHandler}>
      <div className="form-control">
        <label>Course Goal</label>
        <input type="text" onChange={goalInputChangeHandler} />
      </div>
      <Button type="submit">Add Goal</Button>
    </form>
  );
};

export default CourseInput;

 

여기서 사용자가 빈칸을 입력했다면 아이템이 추가되지 않도록 formSubmitHandler를 수정했다.

  const formSubmitHandler = (event) => {
    event.preventDefault();
    if (enteredValue.trim().length === 0) {
      return;
    }
    props.onAddGoal(enteredValue);
  };

입력한 내용을 아이템으로 추가하기 전에 그것이 빈칸인지 아닌지 검사를 한 후, 만약 빈칸이라면 아이템을 추가하기 전에 반환하여 끝내주었다. trim()메소드로 양 끝 공백을 제거하고 그 후 문자열의 길이가 0, 즉 빈칸인지 확인한다.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim

 

String.prototype.trim() - JavaScript | MDN

The trim() method removes whitespace from both ends of a string and returns a new string, without modifying the original string. Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator char

developer.mozilla.org

이렇게 하면 이제 빈칸을 입력한 경우에는 버튼을 클릭해도 아이템 추가가 되지 않는다. 그러나 여기서 끝나서는 안된다. 사용자에게 이에 대한 피드백을 전달해야한다. 그리고 여기에서 스타일링이 필요하다.

 

잘못된 입력값을 넣은 경우 label과 input의 색상을 변경하는 방식으로 사용자에게 피드백을 줄 것이다.

  1. 먼저 입력값이 유효한지 여부를 확인하여 true, false로 나타내는 state을 생성한다.
  2. 입력이 빈칸이라면, state 값을 false로 설정한다.
  3. state가 false인 경우와 true인 경우 스타일을 재설정한다(일단 이번엔 인라인으로 스타일 적용).
  const [isValid, setIsValid] = useState(true);

 

  const formSubmitHandler = (event) => {
    event.preventDefault();
    if (enteredValue.trim().length === 0) {
      setIsValid(false);
      return;
    }
    props.onAddGoal(enteredValue);
  };

 

        <input
          type="text"
          onChange={goalInputChangeHandler}
          style={{
            borderColor: !isValid ? "red" : "black",
            backgroundColor: !isValid ? "salmon" : "transparent",
          }}
        />

이렇게 세 부분을 수정했다.

 

여기까지 하면 빈칸을 입력한 경우, 아래와 같이 스타일이 변한다.

 

그런데 현재 부족한 부분이 있다.

  • 스타일 리셋이 되지 않는다.
  • 기존 css 파일에 있는 스타일들을 inline 스타일로 오버라이드하게 된다(inline 스타일이 최우선으로 적용됨, 기존 색상정보 등의 기본 스타일이 다 css파일에 있음).

먼저 첫번째 부분, 다시 내용이 입력이 되었을 때 state을 변경하여 스타일이 변경될 수 있게 하려면

  const goalInputChangeHandler = (event) => {
    if (event.target.value.trim().length > 0) {
      setIsValid(true);
    }
    setEnteredValue(event.target.value);
  };

input태그의 onChange속성에 지정된 goalInputChangeHandler 함수에 조건문을 넣어서 event가 발생한 element의 value값이 빈칸인지 확인, 그 길이가 0보다 크다면 빈칸이 아니므로 다시 state을 true로 바꿔준다. 그러면 빈칸 입력 후 바뀌어버린 스타일을 다시 바꿀 수 있다.

 

하지만 앞에 언급했듯이 inline 스타일에 대해서는 여전히 아쉬운 점이 있다.

 


3. 동적으로 CSS 클래스 설정하기

그럼 inline 스타일을 사용하지 않고 동적으로 스타일을 주는 방법은 무엇일까.

      <div className="form-control">
        <label style={{ color: !isValid ? "red" : "black" }}>Course Goal</label>
        <input
          type="text"
          onChange={goalInputChangeHandler}
          style={{
            borderColor: !isValid ? "red" : "#ccc",
            backgroundColor: !isValid ? "salmon" : "transparent",
          }}
        />
      </div>

만약 이 <div> state이 false인 경우에만(동적으로) "invalide"라는 클래스를 추가한다면? 그리고 "invalide" 클래스에 대한 스타일을 css에 작성해놓으면? 

 

그럼 먼저 state이 false여서 <div>에 "invalid"클래스가 추가된 상황에 적용될 스타일을 css파일에 추가해 놓자.

.form-control.invalid input {
  border-color: red;
  background-color: #f79c9c;
}

.form-control.invalid label {
  color: red;
}

그리곤 이전에 작성한 inline 스타일은 모두 지운다.

 

이제 동적으로 클래스를 부여하는 방법을 알아야한다.

  1. 동적인 값을 줄 것이기 때문에 {}가 필요하다.
  2. ``(backtick) 사용하여 template literal 형태로 만든다(그러면 ${}안에 javascript 표현식 사용 가능).
      <div className={`form-control ${!isValid ? "invalid" : ""}`}>
        <label>Course Goal</label>
        <input type="text" onChange={goalInputChangeHandler} />
      </div>

그리고 이렇게 수정하면 isValid 값에 따라 동적으로 클래스 이름을 추가, 삭제할 수 있다.

실행하면 올바르게 스타일이 바뀌는 것을 확인할 수 있다.

 


 

이번에 스타일링을 특정 컴포넌트에만 적용되도록 해보자.

현재는 css파일을 컴포넌트에서 import해서 사용하고 있지만, 이 방법은 실제로 해당 css파일이 특정 컴포넌트에만 적용되게 하지는 않는다.

  • styled components 패키지
  • css 모듈

이렇게 두 가지 방법으로 스타일의 적용 범위를 제한할 수 있다.

먼저, styled components부터 알아보자.

 

4. Styled Components 소개

https://styled-components.com/

 

styled-components

Visual primitives for the component age. Use the best bits of ES6 and CSS to style your apps without stress 💅🏾

styled-components.com

npm install --save styled-components

위 명령어를 터미널에 입력하여 styled-components 패키지를 설치한다.

 

이전에 작성했던 버튼 컴포넌트를 styled-components로 스타일링하려고 한다.

(기존 코드)

import React from "react";
import "./Button.css";

 const Button = (props) => {
   return (
     <button type={props.type} className="button" onClick={props.onClick}>
       {props.children}
     </button>
   );
 };

export default Button;

기존 코드는 이렇게 css파일을 import하여 클래스과 태그 이름 selection으로 스타일을 해주었다. 그러나 이렇게 하면 동일한 클래스 이름을 가진 다른 컴포넌트에도 스타일이 적용될 수 있고 그래서 styled-components로 바꿔보겠다.

 

import React from "react";
import styled from "styled-components";

const Button = styled.button`
  font: inherit;
  padding: 0.5rem 1.5rem;
  border: 1px solid #8b005d;
  color: white;
  background: #8b005d;
  box-shadow: 0 0 4px rgba(0, 0, 0, 0.26);
  cursor: pointer;

  &:focus {
    outline: none;
  }

  &:hover,
  &:active {
    background: #ac0e77;
    border-color: #ac0e77;
    box-shadow: 0 0 8px rgba(0, 0, 0, 0.26);
  }
`;

export default Button;

위와 같이 바꾸어주면 된다. 

  1. styled-components 패키지 import하기
  2. styled.button 메소드 안에 스타일 작성
  3. 작성한 스타일이 적용된 버튼 반환

style-componetns는 모든 html elemenent들에 대해 그 element를 반환하는 메소드를 가진다. 각 메소드들은 메소드 이름에 해당하는 html element를 ``(back tick, 백틱) 안에 작성된 스타일을 적용하여 반환한다.

styled-components의 메소드들

styled.[html element이름]`[스타일작성]`

위 구조로 사용하면 된다.

또 스타일 작성시 주의할 점은 hover, active 등의 pseudo class(가상 클래스) 적용 시에는

&:[pseudo-class 이름]{[적용될 스타일]}

이렇게 작성해주어야 한다.

 

개발자도구 elements

개발자 도구를 열어 Button 부분을 살펴보면 class명이 있는 것을 볼 수 있다. styled-components가 고유한 class 이름을 부여한 것이다. 이 때문에 styled-components 사용 시 다른 컴포넌트 스타일은 영향을 받지 않을 수 있는 것이다.

 


5. Styled Components & 동적 props

이번에 CourseInput 컴포넌트의 div를 styled-component로 만들어보자.

import React, { useState } from "react";

import Button from "../../UI/Button/Button";
import "./CourseInput.css";

const CourseInput = (props) => {
  const [enteredValue, setEnteredValue] = useState("");
  const [isValid, setIsValid] = useState(true);

  const goalInputChangeHandler = (event) => {
    if (event.target.value.trim().length > 0) {
      setIsValid(true);
    }
    setEnteredValue(event.target.value);
  };

  const formSubmitHandler = (event) => {
    event.preventDefault();
    if (enteredValue.trim().length === 0) {
      setIsValid(false);
      return;
    }
    props.onAddGoal(enteredValue);
  };

  return (
    <form onSubmit={formSubmitHandler}>
      <div className={`form-control ${!isValid ? "invalid" : ""}`}>
        <label>Course Goal</label>
        <input type="text" onChange={goalInputChangeHandler} />
      </div>
      <Button type="submit">Add Goal</Button>
    </form>
  );
};

export default CourseInput;

form-control 클래스 명의 div를 바꿀 것이다.

 

먼저 앞선 버튼과 마찬가지로 

  1. styled-components 패키지 import하기
  2. styled.div 메소드 안에 스타일 작성
  3. 작성한 스타일이 적용된 div 반환, 사용

아까는 작성한 버튼이 하나의 컴포넌트가 되었는데, 위 div는 CourseInput 컴포넌트의 일부로만 사용될 것이므로 CourseInput.js 파일 안에 div 내용의 또 다른 컴포넌트를 생성하려고 한다.

const FormControl = styled.div`
  margin: 0.5rem 0;

  & label {
    font-weight: bold;
    display: block;
    margin-bottom: 0.5rem;
  }

  & input {
    display: block;
    width: 100%;
    border: 1px solid #ccc;
    font: inherit;
    line-height: 1.5rem;
    padding: 0 0.25rem;
  }

  & input&:focus {
    outline: none;
    background: #fad0ec;
    border-color: #8b005d;
  }

  &.invalid input {
    border-color: red;
    background-color: #f79c9c;
  }

  &.invalid label {
    color: red;
  }
`;

먼저 위 코드를 CourseInput.js 파일 안에(CourseInput 컴포넌트 밖에) 추가하여 특정 스타일을 적용한<div>태그인 FormControl 컴포넌트를 생성하였다. 안에 작성된 스타일을 살펴보면 본래 <div>태그 안에 포함되는 다른 element들의 class 언급 시에도 pseudo-class와 마찬가지로 앞에 &을 추가해주어야 한다.

 

그리고 이렇게 만든 컴포넌트를 JSX코드에서는 아래와 같이 사용하면 된다.

      <FormControl>
        <label>Course Goal</label>
        <input type="text" onChange={goalInputChangeHandler} />
      </FormControl>

 

그냥 이렇게 끝내서는 안되고, 본래 있었던 클래스 동적 변환도 추가해주어야 한다(invalid 클래스 추가). styled-components에 의해 반환된 컴포넌트는 JSX코드에 작성된 해당 컴포넌트에게 모든 props를 전달해주기 때문에 className을 작성할 수 있다.

      <FormControl className={!isValid && "invalid"}>
        <label>Course Goal</label>
        <input type="text" onChange={goalInputChangeHandler} />
      </FormControl>

그래서 이렇게 수정해주면 class명도 동적으로 변환하여 스타일도 동적으로 적용할 수 있다.

 

그런데!

이렇게 동적으로 class명을 변환하여 스타일을 적용하는 방법 외에도 props를 전달하여 변환하는 방법도 있다.

      <FormControl invalid={!isValid}>
        <label>Course Goal</label>
        <input type="text" onChange={goalInputChangeHandler} />
      </FormControl>

이렇게 invalid라는 props로 입력 내용이 유효한지 여부를 나타내는 state인 !inValid를 넘겨주었다. 그러면 입력이 빈칸이 경우에는 invalid 값은 true로 되고, 입력이 제대로 된 경우에는 false 값을 가진다.

그리고 이제 styled-components로 컴포넌트를 생성했던 부분으로 돌아가서 수정을 해보자.

const FormControl = styled.div`
  margin: 0.5rem 0;

  & label {
    font-weight: bold;
    display: block;
    margin-bottom: 0.5rem;
    color: ${(props) => (props.invalid ? "red" : "black")};
  }

  & input {
    display: block;
    width: 100%;
    border: 1px solid ${(props) => (props.invalid ? "red" : "#ccc")};
    background-color: ${(props) => (props.invalid ? "#f79c9c" : "transparent")};
    font: inherit;
    line-height: 1.5rem;
    padding: 0 0.25rem;
  }

  & input&:focus {
    outline: none;
    background: #fad0ec;
    border-color: #8b005d;
  }
`;

props로 전달받은 것의 값에 따라 변환이 필요한 style 속성에 대해서

[style속성]: ${(props)=>(props.[특정속성] ? [true일 경우 style 값] : [false일 경우 style 값])};

이러한 구조로 지정해주면 스타일 변환이 동일하게 이루어진다.

 

개발자 도구 elements

그리고 앞선 button과 마찬가지로 div태그에 고유한 class명이 자동으로 주어진 것을 확인할 수 있다.

 


6. Styled Components & 미디어쿼리

브라우저를 다른 디바이스로 열었을 때 모습

다른 디바이스(모바일)로 열었을 때의 모습을 보면 위와 같다. 그런데 모바일과 같이 화면이 작은 경우에 입력폼 제출 버튼의 너비를 키워 한 줄을 전부 차지하게 바꿔보려고 한다.

이때 styled-components 안에 미디어쿼리 스타일링을 해주면 된다.

const Button = styled.button`
  width: 100%; //추가
  font: inherit;
  padding: 0.5rem 1.5rem;
  border: 1px solid #8b005d;
  color: white;
  background: #8b005d;
  box-shadow: 0 0 4px rgba(0, 0, 0, 0.26);
  cursor: pointer;

  @media (min-width: 768px) { //추가
    width: auto;
  }

  &:focus {
    outline: none;
  }

  &:hover,
  &:active {
    background: #ac0e77;
    border-color: #ac0e77;
    box-shadow: 0 0 8px rgba(0, 0, 0, 0.26);
  }
`;

기본적으로 버튼 너비를 100%로 해주고, 너비 768px이상의 큰 화면에서는 auto로 설정해주었다.

작은 화면
큰화면

이렇게 styled-components 안에 미디어쿼리도 적용할 수 있다.

 


7. CSS 모듈 사용하기

그런데 styled-components를 사용하면 위와 같이 자바스크립트 파일에 css 코드가 들어가게 된다. 이 점이 살짝 거슬리는데.. 그렇다면 css 모듈을 사용하면 된다.

 

https://create-react-app.dev/docs/adding-a-css-modules-stylesheet/

 

Adding a CSS Modules Stylesheet | Create React App

Note: this feature is available with react-scripts@2.0.0 and higher.

create-react-app.dev

 

  1. css파일 이름을 본래 'Button.css'에서 'Button.module.css'로 변경하기
  2. 컴포넌트 파일에서 css파일을 import 하기: import 구문이 특이한데, import styles from "Button.modules.css"; 로 해준다.
  3. 스타일 컴포넌트에 적용하기: className={styles.클래스이름} 으로 지정한다.
import React from "react";
import styles from "./Button.module.css"; //import 구문 주의

const Button = (props) => {
  return (
    <button type={props.type} className={styles.button} onClick={props.onClick}>
      {props.children}
    </button>
  );
};

export default Button;

 

css파일 내용은 아래와 같다.

.button {
  font: inherit;
  padding: 0.5rem 1.5rem;
  border: 1px solid #8b005d;
  color: white;
  background: #8b005d;
  box-shadow: 0 0 4px rgba(0, 0, 0, 0.26);
  cursor: pointer;
}

.button:focus {
  outline: none;
}

.button:hover,
.button:active {
  background: #ac0e77;
  border-color: #ac0e77;
  box-shadow: 0 0 8px rgba(0, 0, 0, 0.26);
}

즉, css파일에서 .button 클래스에 대해서 작성한 스타일링들을 특정 컴포넌트에만 적용할 때에는 해당 컴포넌트의 className 속성에 적용하고자 하는 css 클래스명을 지정해주면 되는 것이다.

 

개발자도구 element - button

그리고 개발자도구에서 Button의 클래스명이 특이한 것을 확인할 수 있다.

컴포넌트이름_css클래스이름__(unique값)

의 형식으로 클래스를 unique한 값으로 자동으로 만들어주어 해당 스타일링이 다른 컴포넌트들에는 영향을 주지 못하게 된다.

 


8. CSS 모듈 사용한 동적 스타일링

이번엔 입력폼 스타일링을 css모듈을 사용해 적용시켜 보자.

아까와 마찬가지로

css파일 이름을 변경해주고, js파일에서 import해주고, 해당하는 컴포넌트의 className 속성에 스타일링을 지정해준다.

      <div
        className={`${styles["form-control"]} ${!isValid && styles.invalid}`}
      >

여기서 주의할 점이 있는데 본래 'styles.클래스이름'으로 css파일의 클래스 스타일링을 적용해주지만, -(dash)가 포함된 클래스명은 그와 같은 방식으로는 유효하지 않은 형식으로 아래와 같은 형식으로 적어주어야 한다.

styles["form-constrol"]

그런데 입력폼 클래스 지정은 위보다 훨-씬 긴 것을 볼 수 있는데 그 이유는 바로

동적 스타일링도 적용해주어야 하기 때문이다.

isValid가 false인 경우에는 css파일의 invalid클래스 스타일링도 적용해주기 위해 ``(backtick)을 사용하여 작성해주었다.

 


여기까지 해서 리액트 컴포넌트 스타일링 끝!!