CSS 고민을 줄여준 개념 5가지 소개 (feat. has, clamp 등)

publishedDate
2025/03/10
type
Post
Side Project
tags
MODDO
CSS
1 more property
MODDO 서비스를 개발하며 새롭게 알게 된 CSS 지식을 소개합니다.

들어가며

사실 지금까지는 디자이너님과 함께 협업하는 경험이 많지 않았기 때문에 CSS 쪽에서 해결할 수 없는 문제가 발생하면 개발자들끼리 ‘어쩔 수 없다’ 라는 무언의 공감으로 가능한 수준까지의 CSS 작업을 하곤 했었는데요. (부끄럽네요) 이렇게 하다 보니 CSS를 계속 기피하게 되고 실력도 늘지 않았습니다.
이번에 MODDO 서비스는 두분의 디자이너분과 함께 작업을 했는데, 만들어주신 디자인이 너무 예쁘고 또 그분들의 기대치 (라고 하면 그 디자인을 서비스에 녹여내는 것이겠죠?)에 부응하기 위해서는 ‘어쩔 수 없다’ 라는 마인드를 절대 가질 수 없고 어떻게든 해야 겠다는 마음만 들더라고요. 다행스럽게도 저와 페어로 작업하신 다른 프론트엔드 개발자분이 CSS에 진심이라 덩달아 저도 즐겁게 CSS 작업을 할 수 있었습니다.
제가 이번에 디자인 요구사항을 맞추기 위해서 열심히 찾아보다가 실제 프로젝트에 너무 잘 쓴 5가지 CSS 개념을 공유해보려고 합니다.

:has()

has는 특정 상태를 가진 요소를 선택할 수 있는 가상선택자 중 하나인데요. “특정 자식 요소를 가지고 있는 부모 요소”를 선택할 수 있는 가상 선택자입니다.
다들 아는 내용이겠지만.. “특정 부모 요소의 자식 요소”를 선택하는 방법
아래와 같이 작성하면 선택자2를 가지고 있는 선택자1에 스타일 코드가 적용됩니다.
선택자1:has(선택자2) { /* 스타일 코드 */ }
CSS
복사
저는 이 has 선택자를 input 태그를 감싸고 있는 Wrapper 컴포넌트에 적용해서 input의 상태에 따라 border를 추가하는 기능에 활용했습니다.
저희 Input 컴포넌트의 일부 코드인데요. input 태그와 아이콘을 감싸고 있는 Wrapper 컴포넌트가 있는 구조입니다.
<S.Wrapper $error={error} $disabled={disabled} className="input-wrapper" > <S.Input autoComplete="off" ref={ref} disabled={disabled} {...props} /> <S.IconWrapper>{icon}</S.IconWrapper> </S.Wrapper>
TypeScript
복사
이 때 Wrapper 컴포넌트에 자식 input이 비어있을 때 (즉 placeholder가 보일 때) border를 추가하려면 has 선택자를 아래와 같이 활용하면 됩니다.
&:has(input:placeholder-shown) { border: ${({ theme }) => `1px solid ${theme.color.semantic.border.default}`}; background: ${({ theme }) => theme.color.semantic.background.normal.default}; }
TypeScript
복사
간단하죠?

:placeholder-shown

위에서 사용한 :placeholder-shown도 이번에 새로 알게 된 가상 선택자 중 하나입니다.
이름에서도 알 수 있듯이 placeholder가 보이는 상태의 요소를 선택하는 선택자인데요.
이 선택자를 이용해서 JS 없이도 입력이 없는 경우와 있는 경우를 다르게 스타일링 할 수 있습니다!
빈 상태인 경우에는 placeholder가 보일 테니 input:placeholder-shown 로 선택해 주면 되고, 입력이 있는 경우에는 placeholder가 보이지 않을테니 input:not(:placeholder-shown) 이렇게 not 연산자로 반대의 경우를 선택해주면 됩니다.
/* 입력값이 있는 경우 (placeholder가 보이지 않는 경우) */ &:has(input:not(:placeholder-shown)) { border: ${({ theme }) => `1px solid ${theme.color.semantic.border.strong}`}; background: ${({ theme }) => theme.color.semantic.background.normal.default}; } /* 입력값이 없는 경우 (placeholder가 보이는 경우) */ &:has(input:placeholder-shown) { border: ${({ theme }) => `1px solid ${theme.color.semantic.border.default}`}; background: ${({ theme }) => theme.color.semantic.background.normal.default}; }
TypeScript
복사
placeholder를 기준으로 빈 값 여부를 확인하는 것이기 때문에 placeholder가 있는 Input에서만 사용할 수 있다는 점만 유의한다면 정말 간단하게 빈 값을 판단할 수 있는 방법이라고 생각합니다.

:focus-within

:focus 선택자는 많이들 써 보셨을 것 같습니다. :focus는 이름 그대로 클릭되는 등의 이벤트로 포커스 된 상태의 요소를 선택하는 가상 선택자인데요.
:focus-within은 자식 요소로 포커스된 요소를 가지고 있는 부모 요소를 선택할 수 있는 가상 선택자입니다.
저는 :focus-within 선택자를 :has를 사용했던 Input 컴포넌트의 요구사항을 위해서 사용했는데요. input 태그가 포커스 된 경우에 input 태그를 감싸고 있는 Wrapper 컴포넌트에 border를 추가하는 코드는 아래와 같습니다.
&:focus-within { border: ${({ theme }) => `2px solid ${theme.color.semantic.orange.default}`}; background: ${({ theme }) => `${theme.color.semantic.background.normal.default}`}; }
TypeScript
복사

min-width: 0

flex box 안에 컴포넌트를 배치할 때 어떻게 해도 자식 요소가 부모 요소의 크기를 넘어버리는 상황이 생기면 스트레스를 많이 받는데요… (경험담입니다…) 놀랍게도 자식 요소에 min-width: 0을 지정하는 것으로 문제를 간단히 해결할 수 있었습니다.
flex box 안의 자식 요소를 flex item이라고 하고, flex item의 기본 속성은 min-width: auto 입니다. 이 설정은 내부 컨텐츠의 크기에 따라서 최소 너비를 조정하는 것이기 때문에 자식 요소의 크기가 커져버리면 어떻게 해도 부모 요소의 크기를 벗어나게 되는 것이죠. 그래서 min-width: 0 을 추가해서 min-width: auto 을 상쇄시켜 주면 문제가 해결됩니다.

clamp

clamp는 반응형 디자인을 적용할 때 도움을 많이 받은 함수입니다. 구조는 아래와 같이 생겼는데요.
clamp(min, val, max) /* min: 최소값 */ /* val: 가변값 */ /* max: 최댓값 */
CSS
복사
clamp 함수로 반환되는 값은 기본적으로 val로 전달한 가변값이 됩니다. 가변값이라 하면 vw, em, rem 같은 상대 단위 값을 말해요.
하지만 valmin에 지정한 값보다 작다면 clamp 함수는 min을 반환하고, 비슷하게 valmax에 지정한 값보다 커지면 max를 반환합니다.
저는 요소 사이 간격을 지정할 때 clamp를 사용했는데, 자료를 찾다 보니 폰트 사이즈를 반응형으로 지정할 때에도 clamp를 유용하게 사용할 수 있다는 것을 알게 되었습니다!

마치며

CSS를 잘 아시는 분들에게는 좀 시시한 글이었을 수 있지만 개인적으로는 이번 프로젝트를 하면서 기록으로 남기고 공유해야 겠다고 생각했던 큰 깨달음 중 하나였습니다. CSS는 피곤한 것이라고만 생각하고 있었는데 위 5가지 내용들을 알게 되어 적용하면서 디자이너님의 요구사항을 딱 맞출 수 있게 되었을 때 ‘이것이 CSS를 좋아하시는 분들의 마음이구나’ 하는 느낌도 받았거든요. 저처럼 CSS를 피곤해하셨던 분들에게 이 글이 도움이 되었으면 좋겠습니다
MODDO가 궁금하다면?
모임 정산 서비스 link iconMODDO