본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Frontend

CSS 선택자와 우선순위

작성 2026. 10. 21.추가
목차14개 항목
출처와 참고

직접 정리한 TIL에 web.dev 「Learn CSS」 강좌의 Selectors·Pseudo-classes·Pseudo-elements·Nesting·Specificity·Inheritance 모듈을 더해 보강한 노트다. 「Frontend_TIL: nth-child 안먹히는 이유」·「CSS 요소 선택 방법」·「효율적으로 CSS 사용」과 「CSS Protips」의 선택자 팁, 「Quantity Queries with Flexbox」의 수량 쿼리를 바탕으로 보강했다. 별점 예시는 「Frontend_TIL: 호버, 클릭시 별점 기능 UI」를 바탕으로 보강했다.

선택자(Selector)는 스타일을 적용할 HTML 요소를 고르는 문법이다. 하나의 CSS 규칙은 선택자 목록 + 선언 블록이고, 선언은 속성과 값의 쌍이다. 한 요소에 여러 규칙이 겹치면 CSS는 캐스케이딩(cascading) 규칙에 따라 하나를 고른다. "왜 내 스타일이 안 먹지?"의 대부분은 이 우선순위 문제다.

기본 선택자

선택자예의미
전체*모든 요소
요소(타입)p, div해당 태그 전부
클래스.boxclass 값 목록에 box가 들어 있는 요소(다른 클래스가 함께 있어도 된다)
아이디#headerid="header"인 요소
속성[lang], [type="text"]특정 속성을 가진 요소
목록h1, h2, .title쉼표로 묶은 것 중 하나라도
  • 클래스와 ID는 숫자로 시작할 수 없다(.1col은 무효).
  • ID가 문서에 중복돼도 규칙은 모두에 적용되지만, ID는 하나여야 하고 특정성이 너무 높아 재사용하기 어렵다. 스타일에는 클래스를 쓴다.
  • 요소 선택자는 해당 태그 전부에 적용된다는 점이 장점이자 단점이다. 태그를 바꾸면 스타일도 사라진다.
  • 선택자를 붙여 쓰면 복합 선택자(compound selector)다: a.my-class, .button.on(상태별 변형).

속성 선택자 연산자

[href*="example.com"] {}   /* 값에 포함 */
[href^="https"] {}         /* 값이 ~로 시작 */
[href$=".pdf"] {}          /* 값이 ~로 끝남 → a[href$=".pdf"]::after { content: " (PDF)"; } */
[class~="card"] {}         /* 공백으로 나뉜 목록 중 하나가 정확히 일치 */
[lang|="en"] {}            /* en 또는 en-로 시작 */
[data-type="primary" i] {} /* 대소문자 무시 */
[data-type="primary" s] {} /* 대소문자 구분 */

결합자(Combinators)

결합자문법의미
자손A BA 안에 있는 모든 B (깊이 무관)
자식A > BA 바로 아래 B
다음 형제A + BA 바로 뒤의 형제 B 하나
뒤따르는 형제A ~ BA 뒤에 오는 같은 부모의 형제 B 전부
div span { color: red; }    /* div 안의 모든 span */
div > span { color: red; }  /* div의 직계 자식 span만 */
.stack > * + * { margin-block-start: 1em; }  /* 직계 자식 사이에만 간격(첫 항목 제외) */
input:checked ~ .switch { background: green; } /* 체크박스 뒤의 커스텀 스위치 */

.top *로 모든 자손에 여백을 주면 중첩된 요소에도 여백이 쌓인다. > * + *처럼 자식 결합자와 다음 형제 결합자를 섞으면 직계 자식 사이에만 준다. * + *는 생김새 때문에 「올빼미 선택자(lobotomized owl)」라고 부르는데, 전역에 쓰면 모든 깊이의 형제에 여백이 붙으므로 위처럼 컨테이너 클래스 아래로 범위를 좁힌다. 조합에 이론상 제한은 없지만 .box > div > h1 + p처럼 깊어질수록 관리가 어려워진다. 실무에서는 3~4단계 이상 이어 쓰지 않는다.

예: CSS만으로 만드는 별점

별 위에 호버하면 그 별과 앞의 별이 모두 칠해지고, 클릭하면 그 상태가 남아야 한다. JavaScript로 호버 인덱스를 state에 두면 마우스가 움직일 때마다 리렌더링되지만, 선택자로 풀면 CSS만 바뀐다. 문제는 ~가 뒤의 형제만 고른다는 점이다.

<!-- 역순(5→1)으로 두고 화면에서만 뒤집는다. 선택 상태는 숨긴 라디오가 기억한다 -->
<fieldset class="rating">
  <input type="radio" id="r5" name="rating" value="5"><label for="r5">5점</label>
  <input type="radio" id="r4" name="rating" value="4"><label for="r4">4점</label>
  <input type="radio" id="r3" name="rating" value="3"><label for="r3">3점</label>
  <input type="radio" id="r2" name="rating" value="2"><label for="r2">2점</label>
  <input type="radio" id="r1" name="rating" value="1"><label for="r1">1점</label>
</fieldset>
.rating { display: flex; flex-direction: row-reverse; justify-content: flex-end; }
.rating input { position: absolute; opacity: 0; }        /* display: none이면 키보드로 못 고른다 */
.rating label:hover,
.rating label:hover ~ label,                              /* 역순이라 「뒤의 형제」가 화면의 앞 별 */
.rating input:checked ~ label { color: gold; }            /* 클릭한 별 뒤(=화면의 앞)까지 유지 */
  • 별 모양은 <label> 안의 SVG로 그리고 색을 fill: currentColor로 받으면 이미지 두 벌(빈 별·찬 별)을 내려받지 않고 색만 바꾼다.
  • 0.5점 단위는 별 반쪽을 한 라벨로 삼아 라벨 수를 두 배로 늘린다.
  • 지금은 :has()로 DOM 순서를 뒤집지 않고 앞의 형제를 고를 수 있다: .rating:has(label:hover) label { color: gold } .rating label:hover ~ label { color: inherit }. 시각 순서와 DOM 순서가 같아 스크린 리더·탭 순서도 자연스럽다.

브라우저는 선택자를 오른쪽 끝(키 선택자)부터 왼쪽으로 맞춰 본다. 그래서 button#back처럼 ID에 태그를 덧붙이거나 ul li a 같은 긴 자손 체인을 피하라는 옛 성능 지침이 있었다. 지금 엔진은 키 선택자로 규칙을 분류하고 결과를 공유해(브라우저 렌더링 과정) 선택자 모양에 따른 속도 차이가 거의 없다. 선택자는 성능보다 특정성과 읽기 쉬움으로 고른다.

가상 클래스: 요소의 상태

가상 클래스(pseudo-class)는 클래스를 동적으로 붙인 것처럼 요소의 상태·위치에 반응한다. 콜론 하나(:)를 쓴다.

묶음가상 클래스맞는 때
상호작용:hover포인터를 올렸을 때(터치에서는 탭한 뒤)
:active누르고 있는 동안
:focus, :focus-visible, :focus-within포커스됨 / 포커스 표시가 필요할 때만 / 자손이 포커스됨
:targetURL 해시(#content)와 id가 같은 요소. 스킵 링크 대상 강조
방문 기록:link, :visited방문 전·후 링크
폼:disabled, :enabled비활성·활성
:checked, :indeterminate체크됨 / 전체 선택 체크박스처럼 중간 상태, 값을 모르는 <progress>
:placeholder-shownplaceholder가 보이는(값이 빈) 입력
:valid, :invalid, :in-range, :out-of-rangeHTML 검증 결과(pattern, min·max)
:user-invalid, :user-valid사용자가 건드린 뒤에만 검증 결과. 처음부터 빨간 테두리가 뜨는 문제를 막는다
:required, :optionalrequired 유무
위치:first-child, :last-child, :only-child형제 중 첫·마지막·유일
:first-of-type, :last-of-type, :only-of-type같은 태그 형제 중
:nth-child(An+B), :nth-last-child(), :nth-of-type()위치 공식. 인덱스는 1부터
기타:empty자식이 전혀 없음(공백 텍스트도 자식이다)
:popover-open열린 팝오버(팝오버와 앵커 위치 지정)
li:nth-child(3n + 3) {}                       /* 3, 6, 9…번째 */
li:nth-child(even) {}                         /* 짝수 */
li:nth-child(-n + 3) {}                       /* 앞에서 1~3번째 (n = 0, 1, 2…를 넣어 3, 2, 1) */
li:not(:nth-child(-n + 3)) {}                 /* 처음 셋을 뺀 나머지 */
.nav li:not(:last-child) { border-inline-end: 1px solid; } /* 마지막을 뺀 항목 사이 구분선 */
tr:nth-child(even of :not([hidden])) {}       /* 숨긴 행을 빼고 센 짝수 행: 필터링되는 표의 줄무늬 */
.post :empty { display: none; }               /* 에디터가 남긴 빈 <p> 숨기기 */
input:user-invalid { border-color: var(--danger); }
  • :nth-child(An+B of S)는 선택자 S에 맞는 형제만 세고 그중 위치를 고른다. li:nth-of-type(even) = :nth-child(even of li)지만 of S는 태그가 아닌 아무 선택자나 받는다.
  • .parent div:first-child는 첫 자식이 <p>면 아무것도 고르지 않는다. "div 중 첫째"는 :first-of-type이다.
  • 같은 함정이 :nth-child에도 있다. <span>1</span><span>2</span><h3>…</h3><span>3</span>에서 span:nth-child(3)은 「세 번째 자식이면서 span」이라 세 번째 자식인 <h3>에서 막혀 아무것도 고르지 않는다. 「span 중 세 번째」는 span:nth-of-type(3) 또는 :nth-child(3 of span)이다. 다른 태그가 섞인 목록을 끝에서 셀 때도 :last-child 대신 :last-of-type·:nth-last-of-type(n)을 쓴다.
  • 「마지막만 빼고 선 긋기」는 모두에 선을 준 뒤 :last-child에서 지우는 대신 :not(:last-child) 한 규칙으로 쓴다. 덮어쓸 규칙이 하나 줄고 읽히는 그대로 동작한다.
  • <div> </div>처럼 공백이 있으면 :empty가 맞지 않는다.
  • 링크는 LVHA 순서로: :link → :visited → :hover → :active. 특정성이 같으면 뒤 규칙이 이기므로 순서가 바뀌면 호버가 방문 스타일에 묻힌다. :visited는 개인정보 보호 때문에 color, background-color, border-color, outline-color, SVG fill·stroke 색만 바꿀 수 있다.
  • 상태를 색만으로 알리지 않는다. 빨강·초록만으로 오류·성공을 표시하면 색각 결핍 사용자가 놓친다. 아이콘과 글을 함께 쓴다(색과 움직임의 접근성).
  • :focus { outline: none }만 쓰면 키보드 사용자가 위치를 잃는다. 포커스 표시는 포커스 표시와 키보드 접근성과 포커스 관리에 있다.

논리 가상 클래스

선택자뜻특정성잘못된 인자가 있을 때
:is(A, B)A나 B 중 하나와 일치. 긴 선택자 목록을 묶는다인자 중 가장 높은 값관대함: 잘못된 것만 무시
:where(A, B):is()와 같지만 덮어쓰기 쉽게항상 0관대함
:not(A)A가 아닌 것인자 중 가장 높은 값엄격함
:has(A)A를 자손(또는 +, ~로 형제)으로 가진 요소. 자식 상태로 부모를 고른다인자 중 가장 높은 값엄격함: 하나라도 틀리면 규칙 전체 무효
.post :is(h2, li, img) {}                        /* .post h2, .post li, .post img 의 축약 */
a:not([class]) { color: blue; }                  /* 클래스 없는 본문 링크 */
img:not([alt]) { outline: 10px solid red; }      /* alt 빠진 이미지 찾기 */
.card:has(img) { grid-template-rows: auto 1fr; } /* 이미지가 있는 카드만 */
form:has(input:valid) label::after { content: "✅"; }
:where(.prose) h2 { margin-top: 2em; }           /* 특정성 (0,0,1)이라 덮어쓰기 쉽다 */
  • 일반 선택자 목록은 하나만 틀려도 규칙 전체가 버려진다. :is()·:where() 안에서는 틀린 것만 무시된다.
  • :has()는 다른 :has() 안에 중첩할 수 없고, 인자에 가상 요소를 쓸 수 없다(:has(::before)는 규칙 전체가 무효).
  • 네 가지 모두 모든 주요 브라우저가 지원한다(:has()는 2023년부터).

수량 쿼리: 형제 수에 따라 스타일 바꾸기

수량 쿼리(quantity query) 는 형제 요소가 몇 개인지 세어 스타일을 바꾸는 선택자 패턴이다. 미디어 쿼리가 화면을 묻는다면 수량 쿼리는 콘텐츠의 양을 묻는다. 항목이 많으면 메뉴를 촘촘하게, 적으면 넓게 펴는 식이다.

/* 과거 방식: 4개 이상이면 모든 li에 적용 */
li:nth-last-child(n + 4),
li:nth-last-child(n + 4) ~ li { flex-basis: 50%; }
 
/* 지금: 부모에서 바로 센다 */
ul:has(> li:nth-child(4)) { --cols: 2; }          /* 4개 이상 */
ul:has(> li:nth-child(4)):not(:has(> li:nth-child(7))) {}  /* 4~6개 */
ul:has(> li:only-child) {}                          /* 정확히 1개 */

:nth-last-child(n + 4)는 「끝에서 세어 4번째이거나 그보다 앞」이라 형제가 4개 이상일 때만 맞고, ~ li로 그 뒤 형제까지 넓힌다. 이 방식은 자식만 고를 수 있어 부모의 레이아웃은 못 바꿨다. :has()가 생긴 뒤로는 부모를 직접 골라 그리드 칼럼 수나 커스텀 속성을 바꾼다. 플렉스박스의 order와 섞으면 항목 수에 따라 특정 항목(예: 가격 칸)을 첫 줄 끝으로 옮길 수도 있지만, 보이는 순서와 읽는 순서가 어긋나지 않게 조심한다(보이는 순서와 소스 순서).

가상 요소: 실제로는 없는 요소

가상 요소(pseudo-element)는 HTML을 건드리지 않고 요소를 하나 끼워 넣거나 요소의 일부를 겨눈다. 콜론 두 개(::)를 쓴다. :before처럼 콜론 하나도 옛 호환을 위해 동작하지만 새 코드는 ::로 쓴다. 개발자 도구에는 보이지만 HTML에는 없다.

가상 요소겨누는 것쓸 수 있는 속성
::before, ::after요소 내용의 맨 앞·맨 뒤에 만든 자식. content가 있어야 생긴다모든 속성
::first-letter첫 글자. 블록 컨테이너에서만(인라인 요소엔 안 됨)색, 배경, 테두리, float, 글꼴, 텍스트 속성
::first-line첫 줄. block·inline-block·list-item·table-cell·table-caption에서만색, 배경, 글꼴, 텍스트 속성
::marker목록 기호, <summary> 화살표색, content, 글꼴, white-space, 애니메이션·전환(CSS 목록과 카운터)
::selection사용자가 선택한 글색, background-color(이미지는 불가), 텍스트 장식·그림자
::placeholder입력의 placeholder 글색, 배경, 글꼴, 텍스트 속성
::backdrop전체 화면·모달·팝오버 아래 깔리는 층모든 속성
::cue, ::cue(b)<video> 자막(WebVTT) 전체·자막 속 요소색, 배경, 글꼴 등
::view-transition-*화면 전환 스냅숏(View Transitions)애니메이션 속성
p::first-letter { float: inline-start; font-size: 2.6em; line-height: 1; margin-inline-end: .2rem; }  /* 드롭캡 */
a[href$=".pdf"]::after { content: " (PDF)"; }
::selection { background: black; color: white; }
  • ::before·::after는 자식을 가질 수 있는 요소에만 붙는다. <img>, <input>, <video> 같은 대체 요소에는 생기지 않는다(체크박스는 예외적으로 된다).
  • content가 빈 문자열이 아니면 스크린 리더가 읽을 수 있다. 장식은 content: ""로 두고, url() 이미지는 원래 크기로만 들어간다.
  • 별도 display가 없으면 인라인으로 취급되고, 텍스트 복사 범위에 포함되지 않는다.
  • ::placeholder는 <label>을 대신하지 않는다(접근 가능한 폼과 오류 처리).

중첩(Nesting)

CSS는 Sass처럼 규칙 안에 규칙을 쓸 수 있다. 모든 주요 브라우저가 2023년 말부터 지원한다(Baseline 2023).

.feature {
  button { color: blue; }          /* = .feature button (기본은 자손 관계) */
  > p { font-size: 1.3em; }        /* = .feature > p */
  + .heading { color: blue; }      /* = .feature + .heading */
 
  &:last-child {}                  /* = .feature:last-child  (& 붙여 씀 → 복합 선택자) */
  & :last-child {}                 /* = .feature :last-child (공백 → 자손) */
  &.highlight {}                   /* = .feature.highlight */
  .theme-dark & {}                 /* = .theme-dark .feature (&를 뒤에) */
 
  @media (min-width: 40em) { padding: 2rem; }
  @container (inline-size > 900px) {}
  @supports (display: grid) {}
  @layer components { h2 {} }
}
button { & + & { margin-inline-start: .5rem; } }   /* 버튼 바로 뒤 버튼 */
규칙내용
&부모 선택자를 뜻한다. 복합 선택자를 만들 때는 필수(&:hover, &.active)
타입 선택자와 붙일 때타입이 앞: p&는 유효, &p는 무효. Sass에서 &p가 .featurep로 이어 붙는 문법과 충돌하지 않게 하려는 규칙이다
특정성&는 :is(부모 목록)처럼 계산된다. #main-header, .intro { & a {} }는 :is(#main-header, .intro) a라 (1,0,1)이다. .intro a(0,1,1)로 덮어쓸 수 없다
가상 요소&는 가상 요소를 나타낼 수 없다. blockquote::before { & {} } 안의 선언은 blockquote에만 적용된다
중첩 가능한 at 규칙@media, @container, @supports, @layer 등 조건부 그룹 규칙. @import는 안 된다
깊이제한은 없지만 2~3단계를 넘으면 특정성과 가독성이 나빠진다

캐스케이딩: 무엇이 적용되는가

<div class="box area">무슨 색일까?</div>
.box { color: red; }
.area { color: blue; } /* 같은 특정성이면 나중에 선언된 것이 이긴다 → 파랑 */

적용 순서는 출처·중요도(!important) → 인라인 스타일 → @layer 순서 → 특정성 → 선언 순서다. 출처(브라우저·사용자·작성자)와 섀도 DOM, 애니메이션·전환까지 포함한 전체 표는 CSS가 동작하는 방식에 있다. 레이어 순서는 특정성보다 먼저라, 뒤 레이어의 단순한 선택자가 앞 레이어의 복잡한 선택자를 이긴다.

@layer reset, base, components, utilities;  /* 뒤로 갈수록 강하다 */
@layer components { #nav .link { color: gray; } }
@layer utilities  { .text-red { color: red; } }  /* 특정성이 낮아도 이긴다 */

선언 순서는 마지막 결정 기준이다. 같은 규칙 안에서 같은 속성을 두 번 쓰면 뒤가 이기고, 브라우저는 이해하지 못한 값을 오류 없이 무시한다. 이것으로 폴백을 만든다.

.title {
  font-size: 1.5rem;                          /* clamp()를 모르는 브라우저용 */
  font-size: clamp(1.5rem, 1rem + 3vw, 2rem);
}

특정성 계산

선택자를 (A, B, C) 세 자리로 센다. 십진수가 아니라 앞자리부터 비교한다. (1,0,0)은 (0,4,3)보다 높다.

자리세는 것0인 것
A (ID급)#id[id="x"]는 속성이라 B다
B (클래스급)클래스, 속성, 가상 클래스:where(), :is()·:not()·:has() 자신(인자 값으로 대체)
C (요소급)요소, 가상 요소*, 결합자
#box {}                          /* (1,0,0) */
.box .area {}                    /* (0,2,0) */
a.my-class.another-class[href]:hover {}  /* (0,4,1) */
article:hover a[href] {}         /* (0,2,2) */
div:not(.my-class) {}            /* (0,1,1) */
:is(h1, h2, #title) {}           /* (1,0,0): 인자 중 가장 높은 값 */
:where(#content) {}              /* (0,0,0): 뒤의 p {} 가 이긴다 */

특정성에 영향을 주지 않는 것:

  • 인라인 style: 특정성이 아니라 그보다 앞 단계(요소에 붙은 스타일)에서 이긴다. 스타일시트로 덮으려면 !important가 필요하다.
  • !important: 특정성을 올리는 게 아니라 선언을 다른 출처(작성자 important)로 옮긴다. 둘 다 !important면 다시 특정성으로 비교한다.

특정성 관리 원칙:

  • 특정성은 필요한 만큼만 높인다. 높게 쌓아 두면 나중에 정말 중요한 스타일을 덮기 어렵다.
  • 불가피하게 올려야 하면 .my-button.my-button처럼 같은 클래스를 반복해 (0,2,0)을 만들 수 있다. 자주 필요하다면 다른 선택자가 너무 구체적이라는 신호다.
  • 리셋·기본 스타일은 :where()로 0으로 두고, 라이브러리는 @layer에 넣어 순서로 다스린다.

상속

부모에 적용한 스타일 중 일부는 자손에게 그대로 전달된다. 상속은 아래로만 흐른다.

상속됨(기본)상속되지 않음
color, font와 font-*, line-height, letter-spacing, word-spacing, text-align, text-indent, text-transform, white-space, direction, visibility, cursor, list-style-*, border-collapse, border-spacing, caption-side, empty-cells, quotes, orphans, widows박스 모델(width, margin, padding, border), background, position·display 관련, animation, opacity 등 나머지

모든 속성은 초깃값(initial value)을 가진다. 캐스케이드로 정해진 값이 없을 때, 상속되는 속성은 부모의 계산값을, 상속되지 않는 속성은 초깃값을 쓴다. 상속되는 속성이라도 브라우저 기본 스타일이 값을 주면 그 값이 이긴다. <strong>이 굵은 이유, <input>·<button>이 글꼴을 물려받지 않는 이유다.

body { font-size: 18px; line-height: 1.5; color: #333; }
input, button, select, textarea { font: inherit; color: inherit; }  /* 폼 요소도 물려받게 */

opacity는 상속되는 속성이 아니다. 부모를 반투명하게 하면 자식까지 함께 그려지기 때문에 상속처럼 보일 뿐이다.

값을 되돌리는 전역 키워드

모든 속성에 쓸 수 있다.

키워드결과쓰는 때
inherit부모의 계산값예외 만들기: .my-component strong { font-weight: inherit; }: 컴포넌트 굵기가 바뀌어도 따라간다
initial속성의 명세상 초깃값(브라우저 기본 스타일 아님)aside strong { font-weight: initial; }: display: initial은 inline이라 주의
unset상속되는 속성이면 inherit, 아니면 initial어느 쪽인지 외우지 않고 지울 때: margin: unset; color: unset;
revert작성자 스타일을 모두 취소하고 사용자·브라우저 기본 스타일로aside p { padding: revert; }: 브라우저 기본값으로
revert-layer현재 레이어의 값만 취소하고 이전 레이어 값으로라이브러리를 낮은 레이어에 넣고 덮어쓴 값을 해제할 때

all 속성은 거의 모든 속성을 한 번에 되돌린다. aside p { all: unset; }은 나중에 전역 p 규칙에 속성이 늘어나도 계속 지운다.

핵심 정리

  • 선택자는 기본 선택자 → 결합자 → 가상 클래스·요소 순으로 확장된다. 위로(부모) 고르는 것은 :has()만 한다.
  • 겹치는 스타일은 출처·!important → 인라인 스타일 → @layer 순서 → 특정성 → 선언 순서로 결정된다.
  • 특정성은 (ID, 클래스, 요소) 세 자리를 앞에서부터 비교한다. 인라인 스타일과 !important는 특정성이 아니라 앞 단계에서 이긴다.
  • :is()·:where()·:has()로 선택자를 묶고, :where()로 특정성을 0으로 낮춘다.
  • 네이티브 중첩에서 &는 :is(부모)처럼 계산된다. 2~3단계 이상 중첩하지 않는다.
  • 글꼴·색상은 상속되고 박스·위치 관련 속성은 상속되지 않는다. inherit·initial·unset·revert·revert-layer로 되돌린다.

관련 노트: CSS가 동작하는 방식: 핵심 개념과 픽셀이 되기까지, 객체 지향 CSS(OOCSS), Sass: 파일 구조, 믹스인과 @extend, CSS 목록과 카운터, DevTools로 DOM과 CSS 다루기, 제약 검증: required·pattern·ValidityState, CSS 실전 팁: 리셋과 작은 패턴

원본 기록14

댓글

GitHub 계정으로 댓글을 남길 수 있어요.