CSS? 그게 뭔데!

목차
StyleSheet가 뭔데?
본 글에 들어가기 전 StyleSheet 를 짚고 넘어가보자.
요즘 우리가 Stylesheet 라고 하면 99%확률로 CSS를 의미한다. 하지만 "Stylesheet = CSS" 라고 표현하면 무언가 위화감이 든다.
" CSS 는 Cascading Style Sheets의 약자인데 그럼 StyleSheet가 더 상위개념이여야 하는 것 아닌가? "
라는 생각이 들었다면 그 생각은 틀리지 않았다. StyleSheet라는 표현은 원래 "문서의 표시(표기) 방식을 정의하는 문서"라는 되게 넓은 개념이다.
그럼 왜 StyleSheet = CSS 로 표현하게 됐을까?
StyleSheet라는 용어는 웹개발의 전유물이 아니다. 가까운 이웃 Unity개발을 들여다보면 Unity Style Sheet라는것이 있다.
하지만, 아무래도 Web 개발자가 많다보니 StyleSheet라는 용어를 사용할때 CSS가 항상 따라왔고 이게 굳어졌다 생각한다.
본 글에선 Web 개발 중심의 CSS와 그 확장형인 SCSS, CSS-In-JS 등을 다룰 예정이다.
쌓아올린 하나의 커다란 케이크, CSS
CSS는 Cascading Style Sheets라는 의미로, 스타일시트 언어의 일종이다.
우리가 평소에 작성하는 CSS 파일을 잘 생각해보면 겹겹이 쌓인 케이크와도 같은 모습을 한다.
- 맨 아래 시트는 브라우저가 제공하는 '기본 스타일'
- 그 위에 우리가 Sheet에 작성한 HTML 태그의 ROOT 규칙을 올리고.
- 다음으로
<body>,<h1>,<p>같은 태그별 스타일을 올려준 후. - 마지막으로 In-line 스타일이나 !important 같은 가장 중요한 장식들을 올려서 꾸며주기.
우리가 마지막으로 보는 화면의 모습은 커다랍고 이쁘게 만들어진 케이크지만. 그 과정에서 우리는 겹겹이 쌓아가며 케이크의 모양을 어떻게 조각할지 계속해서 생각하여야 한다.
CSS는 브라우저 기본스타일 -> 외부 CSS -> 내부 CSS -> 인라인 스타일 순으로 처리된다.
이 처리 과정에서 우리는 아래 세가지 개념을 알아야 한다.
- Inheritance(상속) 부모 요소의 스타일이 자식에게까지 전해지는것
- Specificity(우선순위) Inline > ID > Class > Tag
- Cascade(캐스케이딩) 같은 속성이 여러번 지정되면 마지막으로 지정된 속성만 살아남는것
초콜릿처럼 자연스럽게 퍼진다: 상속 (Inheritence)
케이크 위에 녹은 초콜릿을 부으면 자연스럽게 케이크 전체를 덮게된다. CSS의 일부 속성은 녹은 초콜릿을 부은 것 처럼 자연스럽게 부모 -> 자식 방향으로 퍼져나간다.
상속되는 속성
color, font-",line-height, text-align, visibility, cursor 등
상속되지 않는 속성
margin, padding, border, display, width/height, position 등 Layout / Box와 관련된 속성
필요하다면 상속을 강제할수도 있다.
inherit: 부모의 값을 상속initial: 기본값으로 초기화revert: 바로 이전 계층으로 되돌리기unset: 상속이 되는 속성일 경우 inherit, 그 외에 속성은 initial로 처리
잼 먼저? 크림 먼저?: 우선순위 (Specificity)

영국에서는 전통적으로 뜨거운 논쟁이 있다. 스콘을 먹을때 잼을 먼저 바를지? 크림을 먼저 바를지에 대한 논쟁이다. 결국 스콘과 잼, 크림을 먹는다는 사실은 변하지 않지만. 순서와 위치에 따라서 먹기 전 최종적인 모습이 바뀐게된다.
이 우선순위에 관한건 CSS에 있어서도 중요하다. 동일한 속성을 지정하더라도 우선순위에 따라 결과가 달라진다.
CSS의 우선순위는 아래와 같다. ( 숫자가 낮아질수록 우선순위가 높다 )
- Inline 스타일
- ID 선택자
- Class, 속성
- 태그 선택자 만약 한 라인에 여러 CSS가 올라가게 된다면 우선순위의 순서에 맞춰 적용되게 된다.
문제를 한번 풀어보자
/* CSS */
h1 { color: blue; } /* 태그 */
.title { color: green; } /* 클래스 */
#main { color: red; } /* ID 선택자 */
/* HTML */
<h1 id="main" class="title" style="color: purple;"> Hello World! <h1> /* Inline 스타일 */
위와 같은 코드에서 *"Hello World!"*는 어떤 색으로 렌더링될까?
✅ 정답은 보라색으로 렌더링 된다.
최하위 우선순위인 h1 태그로 color: blue, 파란색이 우선 정의된다
-> 이후 .title 클래스의 color: green, 초록색 으로 덮어씌워 진다.
-> 그 다음 #main ID선택자의 color: red, 빨간색 다시 덮어씌워 진다.
-> 마지막으로 Inline 스타일로 인하여 color: purple, 보라색으로 덮어씌워지며
최종적으로 보라색으로 Hello World! 가 출력된다
막내는 가장 사랑받으니까: 캐스케이딩 (Cascading)
보통 한 집안에 막내가 태어나면, 온 가족의 사랑을 독차지한다는 이야기가 있다. CSS도 똑같다.
하나의 속성이 여러 번 정의되었을 때, 마지막에 선언된 규칙(막내) 이 화면에 적용된다. 앞에서 몇번 규칙을 정의했든, 마지막 값으로 덮어씌워지게 된다.
문제를 한번 풀어보자
/* CSS */
p { color: red; }
p { color: blue; }
p { color: green; }
/* HTML */
<p> 제가 막내에요! </p>
위와 같은 코드에서 *"제가 막내에요!"*는 어떤 색으로 랜더링 될까?
✅ 정답은 초록색으로 렌더링 된다.
가장 먼저 맏이인 color: red, 빨간색 지정된다.
-> 둘째인 color: blue가 정의되면 파란색으로 덮어씌워지게 된다.
-> 막내인 color: green이 정의되면 초록색으로 덮어씌워지게 된다.
CSS 되게 쓰기 편하네요! ..... 아닌가?
css가 짧을때는 되게 편해보인다. 하지만 여러개의 스타일을 컨트롤하며 매우 길어지면 어떨까? 색을 통일하기 위해 시트를 위아래로 여러번 스크롤하고, 반복되는 속성들을 여러번 다시 작성해야하는 과정에 결국 지칠 수 밖에 없다.
이 불편함을 해결하기위해 **Synthetically Awsome Styling Sheet (Sass)**가 등장했다.
Sass는 CSS의 전처리기(Preprocessor) 로, Sass로 StyleSheet를 작성하면 브라우저에서 CSS로 컴파일 후 자동으로 적용해준다.
Sass는 아래와 같은 기능을 제공한다.
- 변수 (Variables)
- 중첩 (Nesting)
- 믹스인 (Mixin)
- 연산 (Operators)
- 상속 (Inheritence / Extend)
- 함수 (fucntion)
- 제어문 (조건 / 반복)
- 모듈 시스템 (Module System)
- 데이터 구조
- 인터폴레이션 (Interpolation)
어딘가 되게 익숙한 향이 나지 않는가? 맞다, Sass는 CSS 에 변수, 함수, 상속등을 추가해 프로그래밍 언어처럼 사용할 수 있게 만들어놓은 확장팩 같은것이다.
한번 각 기능이 어떤 일을 하는지 알아보자,
변수 (Variables)
Sass의 변수는 색상, 간격, 폰트 크기 같은 값을 이름으로 관리할 수 있다.
/* 사용 예시 */
$theme-color: #fff;
$text-color: #000;
button {
background: $theme-color;
color: $text-color;
}
body {
background-color: $theme-color;
color: $text-color;
}
이런 식으로 변수에 값을 담아서 사용하는 장점은 매우 명확하다.
- 재사용성 :
한 번 정의한 값으로 프로젝트 전반에서의 활용 가능
- 디자인 시스템 구축을 통한 디자인 통일성
- 유지보수성 :
특정 디자인 값 변경시 프로젝트 전체에 자동적인 일괄 반영
- 유지보수를 위한 수정, 리딩이 필요한 파일 수의 감소 -> 유지보수 소요 시간 감소.
- 가독성 :
$text-color$main-theme등 HEX코드#nnnnnn이 아닌 문자열 명으로 가독성이 향상된다
이렇게 사용하면 재사용성이 매우 높아지고, 이 색이 어디에 사용되는 색인지 쉽게 알아볼 수 있다는 장점이 있다. 특히, Figma로 디자인을 하는 최근 개발 특성에 맞춰서 Design System을 미리 지정해두고 갈 수 있다는 것 또한 매우 큰 장점이라 생각한다.
네스팅 (Nesting)
Sass에서는 프로그래밍 언어와 같이 중괄호{ } 를 활용하여 CSS 선택자를 중첩하여 작성할 수 있다.
/* 사용 예시 */
nav{
ul{
li{
a{
color: blue;
}
}
}
}
이 SCSS 를 일반 CSS로 표현하면 아래와 같다
nav ul li a {
color: blue
}
이러한 식으로 Nesting 하여 CSS 를 작성하면 몇가지 장점이 있다.
- 프로그래밍 언어와 문법이 비슷하여 CSS에 대한 지식이 없어도 알아보기 쉽다.
하지만 네스팅의 고질적인 문제 중첩이 깊어질수록 가독성이 급격히 나빠진다는 단점또한 맹점이다.
믹스인 (Mixin)
Sass에서는 특정 속성을 변수로 만드는 것뿐이 아닌 코드블럭 단위로 지정을 해줄 수 있다.
/* 사용 예시 */
@mixin flex-center{
display: flex;
justify-content: center;
align-items: center;
}
.container {
@include flex-center
}
위처럼 CSS 를 블럭단위로 @mixin 지시어(Directive)로 정의 후 @include 지시어로 불러서 사용할 수 있다.
연산(Operation)
Sass는 값을 계산하는 연산자 또한 지원한다!
/* 사용 예시 */
.container {
width: 100% / 3; // 33.3333%
margin: 10px * 2; // 20px
color: #333 + #111; // 색상 연산
}
위 처럼 연산자를 이용한값의 지정을 지원하며. 이를 활용하면 더욱 쉬운 반응형 웹 개발이 가능하다
상속(Extend)
Sass는 객체지향 언어와 비슷하게 상속을 지원한다.
/* 사용 예시 */
%button-base {
padding: 10px 20px;
border-radius: 6px;
}
.primary-btn {
@extend %button-base;
background: blue;
}
.secondary-btn{
@extend %button-base;
background: red;
}
위 예시처럼 @extend 지시어를 활용하여 속성을 상속해줄 수 있다.
Mixin 과 Extend 는 뭐가 다른거에요?
다음 기능으로 넘어가기 전 방금 @Extend를 보고 궁금증이 생겻을 것이다. "@Mixin @Extend 둘다 기능을 상속받아서 활용하는 것 같은데 뭐가 다른가요?"
둘의 차이는 컴파일 결과물에서 차이가 생긴다.
@Mixin -> 디렉티브 호출 위치에 코드가 복사됨 @Extend -> 선택자를 병합해 공통된 CSS 블록을 만듬
코드로 한번 보도록 하자.
/* 사용 예시*/
%button-base{
padding: 10px 20px;
border-radius: 4px;
}
@mixin button($bg-color) {
height: 20px;
width: 40px;
background: $bg-color;
}
.primary{
@extend %button-base;
@include button(blue);
}
.secondary{
@extend %button-base;
@include button(red);
}
/* 컴파일 결과 */
.primary, .secondary {
padding: 10px 20px;
border-radius: 4px;
}
.primary{
height: 20px;
width: 40px;
background: blue;
}
.secondary{
height: 20px;
width: 40px;
background: red;
}
코드를 확인해보면 확실히 다른점이 느껴진다.
둘을 어떠한 상황에 사용해야할까? 고민이 된다면 아래와 같은 기준을 적용해보는 것도 좋다.
재사용 / 조건 분기가 필요하면? -> @mixin Directive 공통 규칙을 묶어 코드량을 줄이려면 -> @extend Directive
함수 (Function)
Sass는 내장 함수와 사용자 정의 함수를 @function @Return Directive를 통해 제공한다
/* 사용 예시 */
@function rem($px) {
@return $px / 16px * 1rem;
}
h1 {
font-size: rem(32px); // 2rem
}
p {
font-size: rem(16px); // 1rem
}
평문으로 해석하면 이런 문장이 될 것이다.
"px 값을 넣으면 알아서 rem 단위로 변환해주는 함수를 만들어서 쓰겠다."
이렇게 단위 변환 같이 반복적으로 계산해야 하는 값을 함수로 빼두면, 수치 하나 바꿀 때마다 손으로 계산하지 않아도 된다는 장점이 있다.
제어문 (Control Directives)
프로그래밍 언어처럼 Sass도 조건문과 반복문을 지원한다. 반복적인 스타일을 한 번에 생성할 때 진가가 발휘된다.
/* 사용 예시 - 반복문 */
@for $i from 1 through 5 {
.col-#{$i} {
width: 20% * $i;
}
}
/* 컴파일 결과 */
.col-1 { width: 20%; }
.col-2 { width: 40%; }
.col-3 { width: 60%; }
.col-4 { width: 80%; }
.col-5 { width: 100%; }
/* 사용 예시 - 조건문 */
@mixin theme-color($theme) {
@if $theme == dark {
background: #000;
color: #fff;
} @else {
background: #fff;
color: #000;
}
}
.dark-mode { @include theme-color(dark); }
.light-mode { @include theme-color(light); }
그리드 시스템을 손으로 .col-1부터 .col-12까지 일일이 작성하는 상상을 해보자. Sass의 @for가 있다면 단 3줄로 끝낼 수 있다.
모듈 시스템 (Module System)
파일이 길어지면 분리하고 싶어진다. Sass는 @use와 @forward로 파일을 모듈처럼 관리할 수 있다.
/* _variables.scss */
$primary: #3498db;
$danger: #e74c3c;
/* _buttons.scss */
@use 'variables' as v;
.btn-primary {
background: v.$primary;
}
.btn-danger {
background: v.$danger;
}
과거에는 @import를 썼지만, 지금은 @use가 표준이다. @import는 전역으로 때려박는 방식이라 파일이 많아지면 어디서 왔는지 추적이 안 됐는데, @use는 네임스페이스로 명확하게 출처를 표시한다.
문제를 한번 풀어보자
아래 코드에서 .box의 font-size는 몇 rem으로 컴파일될까?
@function rem($px) {
@return $px / 16px * 1rem;
}
.box {
font-size: rem(24px);
}
✅ 정답은 1.5rem이다.
24 / 16 = 1.5 이므로 .box { font-size: 1.5rem; } 으로 컴파일된다.
근데 React 쓰면 CSS 파일이 좀 불편하지 않나?: CSS-in-JS
Sass까지 배웠으면 이제 진짜 CSS를 잘 쓸 수 있겠지, 싶지만. 컴포넌트 기반 개발을 하다 보면 또 다른 벽에 부딪힌다.
"이 컴포넌트 스타일이 저 컴포넌트에 왜 적용되는거지?"
React나 Vue처럼 컴포넌트 단위로 개발할 때, 전통적인 CSS는 전역 스코프 문제가 생긴다. .button { color: red } 라고 써버리면 프로젝트 전체의 모든 .button에 스타일이 먹어버린다.
이 문제를 해결하기 위해 등장한 것이 CSS-in-JS다.
CSS-in-JS는 말 그대로 JavaScript 안에서 CSS를 작성하는 방식이다. 대표적인 라이브러리로는 styled-components와 Emotion이 있다.
/* 사용 예시 - styled-components */
import styled from 'styled-components';
const Button = styled.button`
background: ${(props) => props.primary ? '#3498db' : '#fff'};
color: ${(props) => props.primary ? '#fff' : '#3498db'};
padding: 10px 20px;
border-radius: 6px;
`;
// 사용
<Button primary>Primary Button</Button>
<Button>Secondary Button</Button>
평문으로 해석하면 이런 문장이다.
"
primaryprop이 있으면 파란 배경, 없으면 흰 배경인 버튼 컴포넌트를 만들겠다."
CSS-in-JS의 장점은 명확하다.
- 스코프 격리: 컴포넌트 단위로 스타일이 캡슐화되어 전역 충돌이 없다.
- 동적 스타일링: props나 state에 따라 스타일을 JS 로직으로 자유롭게 제어할 수 있다.
- Dead Code 제거: 컴포넌트가 사라지면 스타일도 같이 사라진다.
하지만 단점도 있다.
- 런타임 오버헤드: JS가 실행될 때 스타일을 생성하므로 성능에 영향을 줄 수 있다.
- 러닝커브: CSS와 JS를 동시에 알아야 한다.
- SSR 복잡도: Next.js 같은 서버사이드 렌더링 환경에서 설정이 까다롭다.
최근에는 런타임 오버헤드 문제를 해결하기 위해 빌드타임에 CSS를 추출하는 방식인 zero-runtime CSS-in-JS 라이브러리들(Linaria, vanilla-extract 등)도 등장하고 있다.
끝내며
CSS는 "그냥 색이랑 크기 바꾸는 거 아니야?" 라고 생각했다면, 이 글을 읽고 나서 생각이 좀 바뀌었으면 한다.
Cascade, Inheritance, Specificity 라는 세 가지 축으로 동작하는 언어이고. Sass로 프로그래밍 언어처럼 확장할 수 있고. CSS-in-JS로 컴포넌트 단위로 캡슐화할 수도 있다.
결국 CSS도 배울수록 깊어지는 언어다. 처음엔 케이크 한 조각처럼 단순해 보이지만, 파고들면 파고들수록 겹겹이 쌓인 레이어가 보이기 시작한다.
CSS님의 은혜는 생각보다 깊었다.
읽어주셔서 감사합니다.



