배경
React와 Tailwind CSS를 기반으로 LLM을 이용한 챗봇 프로젝트를 진행하면서, 메세지 입력 필드(Input
또는 Textarea
)에 포커스 했을 때 단순히 입력 필드 자체 뿐만이 아니라 이를 감싸는 컨테이너 전체에 포커스를 주는 UI를 구현할 필요가 있었습니다.
사실 매우 간단한 과제이지만, 여러 방법을 정리해보면서 각 방식에 대한 장단점을 파악할 수 있었기에 이를 정리하고 공유합니다.
방법 1 : absolute position
function Input() {
return (
<Textarea
className="relative flex flex-col ... focus:ring-1 focus:ring-offset-0"
...
>
{messages.length === 0 && (
<div className="absolute top-0 left-0 w-full">
<SuggestedActions />
</div>
)}
<div className="absolute bottom-0 left-0 w-full flex justify-end p-2">
{status === "submitted" ? (
<StopButton />
) : (
<SendButton />
)}
</div>
</Textarea>
);
}
처음 떠올릴 수 있는 방법 중 하나는 입력 필드를 최상위 요소로 두고, 그 안에 다른 부가적인 요소들(예: 제안 버튼, 전송/정지 버튼)을 absolute
포지셔닝을 이용해 배치할 수 있습니다
장점
- 간단함
단점
- 코드만으로 UI를 파악하기 어려움
- 하위 요소가 복잡해질 경우 레이아웃 관리가 까다로움
방법 2 : 자식 속성에 따라 CSS적용하기
function Input() {
return (
<div className="flex flex-col ... has-focus:ring-1 has-focus:ring-offset-0">
{messages.length === 0 && <SuggestedActions />}
<Textarea
className="..."
...
/>
<div className="flex w-full justify-end p-2">
{status === "submitted" ? (
<StopButton />
) : (
<SendButton />
)}
</div>
</div>
);
}
부모 div
요소안에 입력필드와 주변 요소를 배치하고, CSS 의사 클래스 :has()를 적용하면 자식 요소가 특정 조건을 만족하는 경우 스타일을 적용할 수 있습니다. 테일윈드에서는 이를 has-*
클래스명으로 적용할 수 있습니다. 따라서, has-focus:
로 자식요소가 포커스 받는 경우에 링 스타일을 적용해줍니다.
만약 자식 요소가 아니라 형제 요소에 따라 스타일을 적용하고 싶다면 다음과 같이 peer
클래스명과 peer-has-*
클래스명을 사용할 수 있습니다. 부모 요소에 따라 스타일을 적용하고 싶다면 group
클래스명과 group-has-*
클래스명을 사용할 수 있습니다.
<div>
<label class="peer ...">
<input type="checkbox" name="todo[1]" checked />
Create a to do list
</label>
<svg class="peer-has-checked:hidden ..."><!-- ... --></svg>
</div>
<a href="#" class="group ...">
<div>
<svg class="stroke-sky-500 group-hover:stroke-white ..." fill="none" viewBox="0 0 24 24">
<!-- ... -->
</svg>
<h3 class="text-gray-900 group-hover:text-white ...">New project</h3>
</div>
<p class="text-gray-500 group-hover:text-white ...">Create a new project from a variety of starting templates.</p>
</a>
참고자료 1 : Tailwind CSS : hover-focus-and-other-states #has
:has()
의사 클래스는 CSS Selectors Level 4에 제안되었으며, 비교적 최근(2023년)에 주요 브라우저에서 지원이 시작되었습니다.
장점
- 가장 정석적인 방법
단점
- IE같은 구형 브라우저 지원 X
방법 3 : 상태로 구현하기
import { useState} from 'react';
import clsx from 'clsx';
function Input() {
const [isFocused, setIsFocused] = useState(false);
return (
<div className={clsx("...", {
isFocused : "ring-1 ring-offset-0"
}>
{messages.length === 0 && <SuggestedActions />}
<Textarea
className="..."
onFocus={()=> setIsFocused(true)}
onBlur={()=> setIsFocused(false)}
...
/>
<div className="flex w-full justify-end p-2">
{status === "submitted" ? (
<StopButton />
) : (
<SendButton />
)}
</div>
</div>
);
}
같은 UI를 자바스크립트(리액트)를 사용해서 구현할 수 있습니다. onFocus
와 onBlur
에 이벤트에 따라 상태를 변경하고, 이에 따라 조건부로 스타일을 적용합니다.
장점
- 간단함
- IE 지원
단점
- 불필요한 자바스크립트
결론
2번 방법을 선택하였다.
후기
CSS의 매력에 스며들게 되었다. 의사 클래스를 더 사용할 기회가 있으면 좋겠다.