커스텀 체크박스 만들기 with React

윤슬기·2022년 3월 27일
25
post-thumbnail

서비스를 만들 때 프론트엔드단에서 자주 구현하게 되는 구성 중 하나가 입력폼이다. 사용자로부터 입력받는 정보가 다양해지면서 HTML 요소들도 점점 기능과 모양이 다양해졌다. <Input> 요소는 현재 스무 개가 넘는 타입과 그에 맞는 다양한 UI를 갖추고 있다.

그렇게 마련된 요소의 UI를 그대로 입력폼에 사용할 수 있다면 고민할 부분이 별로 없다. 입력 상태에 따라 다르게 나타나는 UI, focus 상태 표시, 접근성 증대 장치 등 많은 것들이 이미 그 안에 구현되어있다. 하지만 브라우저별로 UI가 상이하고, 프로젝트에서는 프로젝트 전체 디자인과 어울리는 양식 UI를 원하는 경우가 대부분이기에 별도로 다양한 처리를 통해 디자인을 구현해야 하는 경우와 맞닥뜨린다.

대표적으로 체크박스(<input type=”checkbox” />)는 색상이나 크기를 바꿔야 하는 일이 잦다. 그래서 현재 실무에서 자주 사용하는 툴인 React와 styled-component를 이용해, 원하는 디자인을 구현하면서도 최대한 원래 요소가 갖춘 장점들을 가져갈 수 있는 방법을 찾아보았다.

원하는 디자인으로 체크박스 구현하기

1. appearnce: none;

appearnce 속성은 브라우저가 기본적으로 보여주는 UI를 조정하고 싶을 때 사용한다. 기본 스타일이 없는 요소에 특정 스타일을 부여하거나, 기본 스타일을 가진 요소의 스타일을 바꾸거나 없앤다. 모든 요소에 적용이 가능한 것은 아니며, 브라우저마다 동작이 조금씩 다르므로 속성 사용 시 지원 대상 브라우저 테스트가 필요하다.

appearnce 속성에 none 값을 설정하면, <select>는 우측 화살표가 보이지 않고, <button>은 둥글게 처리된 모서리 스타일이 사라진다. 그리고 <input type=”checkbox”> 는 체크박스 영역에 아무것도 나타나지 않는다.

이 속성을 이용해, 원하는 디자인을 구현해본다.

  1. label 문자열을 받아 화면에 나타내는 기본 체크박스 컴포넌트를 작성한다.

    import React from "react";
    import styled from "styled-components";
    
    function Checkbox({ text }) {
      return (
        <StyledLabel htmlFor={text}>
          <StyledInput type="checkbox" id={text} name={text} />
          <StyledP>{text}</StyledP>
        </StyledLabel>
      );
    }
    
    export default Checkbox;
    
    const StyledInput = styled.input``
    
    const StyledLabel = styled.label`
      display: flex;
      align-items: center;
      user-select: none;
    `;
    
    const StyledP = styled.p`
      margin-left: 0.25rem;
    `;
  1. 체크박스의 스타일을 없앤다.

    (...)
    
    const StyledInput = styled.input`
      appearance: none;
    `;
    
    (...)
  1. 체크박스가 체크되어있지 않을 때 화면에 보일 스타일을 지정한다.

    (...)
    
    const StyledInput = styled.input`
      appearance: none;
      width: 1.5rem;
      height: 1.5rem;
      border: 1.5px solid gainsboro;
      border-radius: 0.35rem;
    `;
    
    (...)
  2. 체크박스가 체크된 상태(:checked)일 때 어떤 모습을 보여줄지 작성한다. 여기서는 체크 시 체크박스를 연두색 배경으로 바꾸고, 백그라운드 이미지에 ‘v’ 모양 SVG가 보이도록 했다. SVG 코드는 tailwind css 공식 홈페이지에 사용된 코드를 사용했다.

  • checked | tailwindcss
    const StyledInput = styled.input`
      appearance: none;
      border: 1.5px solid gainsboro;
      border-radius: 0.35rem;
      width: 1.5rem;
      height: 1.5rem;
    
      &:checked {
        border-color: transparent;
        background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.707 7.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0-1.414-1.414L7 8.586 5.707 7.293z'/%3e%3c/svg%3e");
        background-size: 100% 100%;
        background-position: 50%;
        background-repeat: no-repeat;
        background-color: limegreen;
      }
    `;

구현된 모습. 하지만 appearance 속성은 IE11에서 지원하지 않는다. 만약 IE까지 대응해야 한다면 다른 방법을 찾아야 한다.

2. <label> 에 가상 선택자로 체크박스 만들기

<label> 엘리먼트는 <input>과 한 쌍으로, <input type=”checkbox”>의 경우 label을 클릭하면 연결된 checkbox가 체크되거나 해지된다.

<input type="checkbox" id="yes" name="yes">
<label for="yes">이 글자를 클릭하면 체크됩니다.</label>

<label>의 가상선택자인 ::after::before로 만든 영역은 label의 연장선이므로, 해당 영역을 클릭해도 동작이 같다. 그를 이용해서 체크박스 영역을 구현해본다.

가상 선택자로 체크박스 영역을 만들 것이므로 우선 <input> 엘리먼트를 화면에서 보이지 않도록 처리해야 한다. 간단하게 display: none을 사용할 수도 있으나, 그렇게 설정하면 엘리먼트가 화면에서 완전히 사라짐과 동시에 스크린 리더 등의 기기에서도 사라지므로 ‘화면상에는 보이지 않지만 기기로 접근할 수 있도록' 처리할 필요가 있다.

2-1. visually-hidden : 겉보기에만 엘리먼트 숨기기

구글로 찾아보면 여러 가지 방법이 제시되어있는데, The A11Y Project 에서 제시하는 숨김법을 적용해보았다.

clipclip-path는 대상이 보일 부분을 지정하는 속성으로, 해당 속성을 이용해 영역이 보이지 않도록 값을 부여한다. clip-pathclip보다 최신 속성으로 추천되는 방법이나 IE에서 동작하지 않으므로 두 가지를 모두 선언해준다.

/* visually-hidden */
.classname {
	position: absolute;
	clip: rect(0 0 0 0);
	clip-path: inset(50%);
	width: 1px;
	height: 1px;
	overflow: hidden;
	white-space: nowrap;
}
  1. 위에서 작성한 것과 같이 기본 체크박스 컴포넌트를 작성한다. input 엘리먼트에 visually-hidden 스타일을 적용한다.
import React from "react";
import styled from "styled-components";

function Checkbox({ text }) {
  return (
    <>
      <StyledInput type="checkbox" id={text} name={text} />
      <StyledLabel htmlFor={text}>
        <StyledP>{text}</StyledP>
      </StyledLabel>
    </>
  );
}

export default Checkbox;

const StyledLabel = styled.label``;

// visually-hidden
const StyledInput = styled.input`
  position: absolute;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  white-space: nowrap;
  width: 1px;
`;

const StyledP = styled.p`
  margin-left: 0.5rem;
`;
  1. lable 엘리먼트의 가상선택자 ::before에 체크 전 체크박스 디자인을 작성한다.
(...)

const StyledLabel = styled.label`
  position: relative;
  display: flex;
  align-items: center;
  user-select: none;

  &:before {
    content: "";
    height: 1.5rem;
    width: 1.5rem;
    background-color: white;
    border: 2px solid gainsboro;
    border-radius: 0.35rem;
  }
`;

(...)
  1. 체크박스가 체크된 상태의 모습은 가상선택자 ::after에 작성한다. 체크 시 ::after 영역이 나타나도록 opacity 속성을 0으로 준다.
(...)

const StyledLabel = styled.label`

	(...)

  &:after {
    opacity: 0;
    content: "";
    position: absolute;
    height: 1.5rem;
    width: 1.5rem;
    border: 2px solid transparent;
    border-radius: 0.35rem;
    background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.707 7.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0-1.414-1.414L7 8.586 5.707 7.293z'/%3e%3c/svg%3e");
    background-size: 100% 100%;
    background-position: 50%;
    background-repeat: no-repeat;
    background-color: limegreen;
  }
`;

(...)
  1. input 엘리먼트가 체크상태일 때(:checked) ::after 영역이 보이도록 ::after 영역의 opacity 속성을 1로 설정한다.
const StyledInput = styled.input`
  position: absolute;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  white-space: nowrap;
  width: 1px;

  &:checked + ${StyledLabel} {
    :after {
      opacity: 1;
    }
  }
`;

이렇게 작성하면 외형적으로 1번과 같은 결과를 얻을 수 있다.

2-2. focus시 outline보여주기

하지만 위 방법에는 문제가 있다. input 엘리먼트를 숨겼기 때문에, tab 키 등 키보드를 조작해서 체크박스에 접근하면 내가 어떤 체크박스를 선택하고 있는지 표시가 되지 않는다는 점이다. 선택 위치를 알 수 없어 불편하다.

포커스가 어디에 위치했는지 나타내는 외곽선 등을 ‘포커스 링'이라고 한다. input 엘리먼트에 포커스가 갈 때, ::before로 만든 체크박스에 포커스 링을 표시해야 한다. outline 속성을 설정해서 영역 주변에 푸른 선이 보이도록 설정한다.

const StyledInput = styled.input`

	(...)

	&:focus + ${StyledLabel} {
     &:before {
        outline: 2px solid blue;
     }
   }
`;

이렇게 설정하면 포커스 시 위치를 잘 알 수 있다. 하지만 키보드 조작을 통해서 포커스를 이동할 때뿐 아니라, 마우스나 터치 등으로 체크박스를 선택할 때에도 포커스링이 나타난다. 이런 상황을 원치 않을 때가 많다. 그래서 키보드 이용시에만 포커스링이 나타나고, 클릭 시에는 나타나지 않도록 처리하고자 한다.

2-2-1. :focus-visible

:focus-visible은 브라우저가 해당 엘리먼트의 포커스 상황을 표시해야겠다 판단했을 때 유효한 의사 클래스다. :focus-visible에 설정한 값은 키보드로 포커스를 이동했을 때만 적용되며, 마우스 클릭과 터치 등에는 반응하지 않는다.

:not() 가상 클래스는 괄호 안에 특정한 클래스를 넣어 ‘해당 클래스가 아닌'것을 선택할 수 있다. 이를 이용해 focus시, :focus-visible 선택자에 해당하지 않는 경우 체크박스 영역에 포커스링이 나타나지 않도록 작성한다.

&:focus:not(:focus-visible) + ${StyledLabel} {
    :before {
      outline: none;
    }
  }

키보드로 접근 시 포커스링의 모습. 이렇게 설정하면 키보드로 엘리먼트에 접근 시에만 포커스 스타일이 나타나고, 마우스나 터치 등으로 접근 시에는 스타일이 나타나지 않는다.

하지만 :focus-visible은 IE에서 지원하지 않는다. 만약 IE를 지원해야 한다면, 같은 동작이 구현되도록 만든 라이브러리 ‘focus-visible’을 사용한다.

2-2-2. 리액트 프로젝트에서 focus-visible 라이브러리 사용하기

리액트 프로젝트에서 focus-visible 라이브러리를 사용하려면 아래 순서대로 세팅한다.

  1. 라이브러리를 인스톨한다.
yarn add focus-visible
  1. App.js 파일 등 프로젝트 랜더 시작점에 라이브러리를 import 한다.
// App.js
import 'focus-visible'; // use focus-visible library
  1. 라이브러리의 기본 사용 문법은 아래와 같다. :focus-visible 클래스가 .focus-visible 클래스로 대체된 형태이다.
.js-focus-visible :focus:not(.focus-visible) {
  outline: none;
}
  1. 위의 문법을 global style, 혹은 인풋 컴포넌트에 적용한다.
/* mouse click시 focus 스타일링 */
.js-focus-visible input {
  &:focus:not(.focus-visible) + label {
    &:before {
      outline: none;
    }
  }
}

/* 키보드 접근 시 focus 스타일링 */
.js-focus-visible input.focus-visible {
  &:focus + label {
    &:before {
    outline: 2px solid blue;
    }
  }
}

최종 코드

최종 코드는 아래 링크에서 확인할 수 있다.

덧붙임: 포커스 링 관련 브라우저 변화

이전에는 크롬 브라우저에서 마우스로 인터렉티브 요소를 클릭할 때 포커스 링이 나타났다. 그래서 그것이 마음에 들지 않는다면 여러가지 방법을 사용해 숨겼다. 그러나 21년 4월 출시한 Chrome 90에서부터 해당 요소 포커스 선택자를 :focus-visible로 변경해, 마우스 접근 시에도 포커스링이 나타나지 않게 되었다.

profile
👩🏻‍💻

1개의 댓글

comment-user-thumbnail
2024년 7월 2일

감사합니다! 큰 도움이 됐습니다!

답글 달기