이번 시간에는 Custom Checkbox를 만드는 방법과 이 때 나타난 여러 이슈에 대해 적어볼까 합니다. 회사에 퍼블리셔 과장님이 계셨어서 그 동안 HTML, CSS를 소홀히 했는데 앞으로는 프론트엔드 개발자가 퍼블까지 기본적으로 다 할 줄 알아야 하기 때문에 다시 분발해야겠습니다. 💪
HTML에서 기본적으로 제공해주는 input checkbox를 사용해보면 다음과 같은 디자인이 나옵니다. 근데 실제 서비스 하려는 화면에서 이런 체크박스를 쓰지는 않을것입니다. 하..하..
따라서 우리는 Custom Checkbox를 만들어보도록 할 것입니다. 저는 총 6가지 종류의 체크박스를 만들어보도록 하겠습니다.
일단, HTML 기본 구조부터 생각해볼까요? 저는 체크박스와 텍스트를 한 묶음으로 사용할 생각으로 label을 감싼 형태로 만들었습니다. 근데 생각해보면 label을 보통 이런식으로 감싸기도 하나...? 라는 의구심이 들어 찾아보니 이것을 '암시적 작성 방법'이라고 하더군요. 이 때는 label for이랑 input id를 작성해줄 필요는 없습니다. (오호...! 새로운 사실을 알아간다)
<label>
<input type="checkbox" />
<div> {/* checkbox 박스 디자인 */}
<Icon name="check"/>
</div>
<span>{text}</span>
</label>
보통 '명시적 작성 방법' 이라고 하면 input 따로, label 따로 만들고 나서 label for이랑 input id를 연결하는 구조로 만드는 것이 특징입니다. 이래야 접근성 측면에서도 좋고, label 클릭 시에도 input 체크박스가 잘 동작하기 때문입니다.
<input id="checkbox" type="checkbox" />
<label for="checkbox">{text}</label>
체크박스의 체크는 svg 아이콘(check.svg)을 사용할 것입니다. 형식은 다음과 같습니다.
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.1562 4.71899L6.59375 11.2812L3.3125 8.00024" stroke="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
그리고 저는 저번시간에 SVGR을 사용해서 svg 파일을 리액트 컴포넌트 처럼 사용할 수 있도록 설정했습니다. 따라서 저는 vector 폴더에 svg 파일을 모아놓고 export 시켜서 사용하도록 했습니다.
import check from "./check.svg";
import kakao from "./kakao.svg";
import close from "./close.svg";
export { check, kakao, close };
그리고 이렇게 export 된 svg 컴포넌트를 불러와서 Icon 컴포넌트 형태로 사용하도록 만들어줍니다.
import * as vectors from "./vectors";
type IconName = keyof typeof vectors; // "check" | "kakao" | "close" ...
interface IconProps {
name: IconName;
className?: string;
}
function Icon({ name, className }: IconProps) {
const Vector = vectors[name];
return <Vector className={className} />;
}
export default Icon;
일단, 초기에는 이런식으로 Checkbox 컴포넌트를 만들었습니다.
props로 checked와 onToggle을 받아서 상태는 외부에서 처리하도록 위임했고, text props를 통해 checkbox 옆에 텍스트를 작성할 수 있도록 했으며, size 조절도 가능하고, disable 상태에 따라 디자인이 달라지도록 했습니다.
크기 조절 같은 경우는 부모 요소(label)의 글꼴 크기에 따라 체크박스 크기도 조절될 수 있도록 'em' 단위를 사용했습니다.
import { ReactElement } from "react";
import { cn } from "@/lib/utils";
import Icon from "../icon";
interface CheckboxProps {
checked: boolean;
onToggle: () => void;
text?: string | (string | ReactElement)[];
size?: "sm" | "md" | "lg";
disabled?: boolean; // 선택 불가능
}
function Checkbox({
checked,
onToggle,
text,
size = "md",
disabled,
}: CheckboxProps) {
return (
<label
className={cn(
"relative inline-flex items-start gap-2",
size === "sm" && "text-sm",
size === "md" && "text-lg",
size === "lg" && "text-3xl"
)}
>
<input
type="checkbox"
checked={checked}
onChange={onToggle}
className="absolute left-0 top-0 w-0 h-0"
disabled={disabled}
/>
<span
className={cn(
"relative flex justify-center items-center border w-[1.25em] h-[1.25em] flex-none",
disabled && "bg-[#D1D1D1] border-none",
checked && `bg-[#111111] border-none`
)}
>
<Icon
name="check"
className={cn(
checked || disabled ? "stroke-white" : "stroke-[#D1D1D1]"
)}
/>
</span>
{text && <span>{text}</span>}
</label>
);
}
export default Checkbox;
이렇게 해서 잘 되는 것처럼 보였습니다만 몇가지 이슈가 있었습니다.
이래서 프론트엔드 개발자는 브라우저와 폰기종마다 테스트를 해야하나 봅니다. 저는 크롬, 웨일 브라우저 애용자라 해당 브라우저에서 잘 되니까 배포를 했는데, 아이폰으로 보니 check svg가 나오질 않더군요.
참고 : https://seons-dev.tistory.com/entry/사파리에서-SVG가-나타나지-않을때-해결방법
해당 이슈에 대해 찾아보니 아무래도 사파리 브라우저 혹은 iOS 자체 문제인거 같습니다. 하여튼 규격 잘 안지키는 애플이 문제지.
“참고로 사파리에서는 svg가 렌더링 하는 범위가 달라지는데, 크롬의 경우 width="10px"이라고 가정할 때 10px로 정상적으로 인식하는 반면에 사파리에서는 1000%로 인식한다고 합니다.” 라고 합니다.
그래서 해결방법으로는 svg에 style="width:100%;height:100%;" 를 주면 됩니다.
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:100%;">
수정했더니 체크 아이콘은 이제 잘 보입니다. 하지만 저 조그만 네모는 도대체 뭘까요? 산넘어 산이네요.
회사에서는 윈도우 환경, 안드로이드 웹뷰에서만 작업했었다보니 사파리에서 개발자도구를 제대로 켜본적이 없는거 같네요. 나는 물경력인가...ㅋㅋ
참고 : https://heinafantasy.com/90
사파리 설정에 들어가서 고급에서 "메뉴 막대에 개발자용 메뉴 보기"를 선택해줍니다.
그러면 메뉴바에서 개발자용이란 탭이 생긴것을 볼 수 있습니다.
페이지 소스 보기를 하면 크롬 개발자도구처럼 확인해볼 수 있습니다. 진짜 input을 없애버리니 문제가 해결이 됩니다. input이 문제였군요.
input에 그냥 display: none; 으로 해도 해결이 되는 문제이긴 하지만 웹 접근성 입장에서는 좋지 못한 방법이라고 합니다. 왜냐면 시각장애인을 위한 스크린리더에서 인식하지 못하기 때문에 웹 접근성에서 매우 좋지 않은 방법입니다.
그래서 찾아보시면 다양한 해결방법이 있는데 그 중에서 다음과 같이 clip을 사용하는 방법이 대표적입니다.
._hidden {
position: absolute;
display: block;
width: 1px;
height: 1px;
overflow: hidden;
white-space: nowrap;
clip: rect(1px 1px 1px 1px);
clip-path: inset(1px);
}
zero를 사용하지 않고 width, height를 1px로 하여 사라지지 않도록 해주면서, clip을 사용해서 지정된 클리핑 범위의 바깥 부분을 숨겨주도록 했습니다.
<input
type="checkbox"
checked={checked}
onChange={onToggle}
className="_hidden"
disabled={disabled}
/>
굳~ 이제 잘 됩니다.
마지막으로 한 가지 더... 지금 잘 보면 checkbox와 span text 사이에 미세한 높이 차이가 있어보입니다.
flex로 감싸고 items-start 를 사용했는데도 이러니까 어떻게 해야할지 모르겠더군요. 근데 해결방법은 여러가지가 있습니다.
가장 대표적으로 그냥 체크박스에 position: absolute를 설정해서 top을 세부조정하는 것입니다.
그게 아니면 checkbox를 나타내는 span 태그를 div 태그로 바꾼다음 margin-top을 줘서 세부조정할 수도 있을 것입니다. 참고로 span 태그와 같은 inline 엘리먼트는 width와 height 속성을 지정해도 무시되며, margin과 padding 속성은 좌우 간격만 반영이 되고, 상하 간격은 반영이 되지 않습니다.
저는 간단하게 2번째 방법으로 조정했습니다.
<div
className={cn(
"relative flex justify-center items-center border w-[1.25em] h-[1.25em] flex-none mt-[3px]",
disabled && "bg-[#D1D1D1] border-[#D1D1D1]",
checked && `bg-[#111111] border-[#111111]`
)}
>
<Icon
name="check"
className={cn(
checked || disabled ? "stroke-white" : "stroke-[#D1D1D1]"
)}
/>
</div>
휴... 이제야 어느정도 편~안 하네요.
이번 시간에는 HTML, CSS 관련해서 얻어가는게 많은 시간이었던거 같습니다. 이제 경력 1~2년인 프론트 개발자이면서 부족한게 아직 많아서 부끄럽기도 합니다. 그럴수록 분발해야겠죠? 💪