고윤태의 개발 블로그

누구나 접근하기 쉽지만 얻어 가는 것이 있는 글을 쓰기 위해 노력 중입니다.

개발

React Native 나만의 checkbox 구현하기 ✅

고윤태 2023. 5. 16. 14:48

안녕하세요 이번에 작성하게 될 내용은 제가 react-native에서 저만의 checkbox를 어떤 계기로 구현하게 되었고 어떤 식으로 구현했는지에 대해 전달을 하고자 작성해 봅니다

개발 환경

React-native(expo) + TypeScript로 진행되었습니다.


구현하게 된 계기

요즘 퇴근 후 토이 프로젝트를 진행하고 있습니다.
checkbox를 사용하는 UI가 존재하여 처음에는 checkbox를 사용하기 위해 React-Native 공식 문서에 가서 확인해 보니 이런 문구가 존재했습니다.

출처(https://reactnative.dev/docs/checkbox.html)

제거가 됐다 커뮤니티의 패키지를 사용해라


그래서 저는 제가 사용할 라이브러리를 찾기 시작했고 눈에 들어온 것은 
이것이었습니다.

 

출처(https://docs.expo.dev/versions/latest/sdk/checkbox/)

덕분에 편하게 하겠다는 마음에 expo-checkbox를 사용하여 디자인 시스템 와이어 프레임의 checkbox처럼 구현을 해놨습니다.

와이어 프레임 checkbox

사용법도 매우 간단하여 마음에 들었습니다.

import Checkbox from 'expo-checkbox';
import React, { useState } from 'react';
import { Text, View } from 'react-native';

export default function App() {
  const [isChecked, setChecked] = useState(false);

  return (
      <View>
        <Checkbox
          style={styles.checkbox}
          value={isChecked}
          onValueChange={setChecked}
          color={isChecked ? '#4630EB' : undefined}
        />
        <Text>checkbox</Text>
      </View>
  );
}

 

하지만 제대로 된 디자인 시스템의 checkbox는 제가 예상하던 것과 너무 달랐습니다.

 

디자인 시스템 checkbox

 

이런 식으로 checkbox가 On 시 disabled 시에 컬러를 변경해야 했습니다.
또한 border-color도 변경해야 했으며 check 시 check 모양의 아이콘도 저희 디자인 시스템에 있는 것을 사용해야 했습니다.

저는 이 작업은 우선순위가 높지 않다 판단하여 시간이 남을  새로 다시 구현하기로 하고 TODO를 작성해놨습니다.

 

TODO 작성을한 지 일주일 정도의 시간이 지났고 이번에 시간이 남아서 구현해 보도록 하겠습니다.


기능 추출

제가 생각하는 checkbox의 기능은 다음과 같습니다.

선택 상태를 표시하는 UI
사용자가 체크박스를 선택하면 선택됐다 표시가 되고 다시 한 번 더 선택하면 표시가 없어집니다

구현

props

interface Props {
  isChecked: boolean;
  disabled?: boolean;
  onValueChangeHandler?: (checked: boolean) => void;
  children?: React.ReactNode;
  style?: ViewStyle;
}

 

  • isChecked : check의 상태를 받습니다.
  • disabled : 비활성화 여부를 받습니다.
  • onValueChangeHandler : check 여부를 변경하려고 유저가 check 박스를 눌렀을 때 발생할 callback 함수를 받습니다.
  • children : checkbox 옆에 Text(Label)을 사용하는 것이 일반적이기 때문에 그것을 받는 용도로 작성해놨습니다.
  • style : checkbox의 margin 또는 padding width 등을 조절할 때 사용하기 위해 넣었습니다.

코드

 

import React from 'react';
import { StyleSheet, View, Pressable, ViewStyle } from 'react-native';
import Check from './icons/Check';
import Color from '../../constants/color';

interface Props {
  isChecked: boolean;
  disabled?: boolean;
  onValueChangeHandler?: (checked: boolean) => void;
  children?: React.ReactNode;
  style?: ViewStyle;
}

const UiCheckbox = (props: Props) => {
  const { isChecked, disabled, onValueChangeHandler, children, style } = props;

  const onPressedHandler = () => {
    if (onValueChangeHandler) {
      onValueChangeHandler(!isChecked);
    }
  };

  return (
    <View style={[styles.container, style]}>
      <Pressable
        disabled={disabled}
        onPress={onPressedHandler}
        style={[
          styles.checkbox,
          isChecked && styles.checked,
          disabled && styles.disabled,
          isChecked && disabled && styles.checkedAndDisabled,
        ]}
      >
        {isChecked && (
          <Check size={16} color={disabled ? Color.neutral3 : Color.white} />
        )}
      </Pressable>
      {children && (
        <View style={styles.label}>
          {children}
        </View>
      )}
    </View>
  );
};

export default UiCheckbox;

 

코드 설명

Pressable을 이용하여 box 형태의 UI를 구성 후 props의 isChecked, disabled 값을 사용하여 스타일링을 추가해 주고 있습니다.
check icon은 props의 isChecked가 true라면 렌더링 시키고 있습니다.
children이 있는 경우 View 태그 안에 자식들을 렌더링 시켜주고 있습니다.

 

const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  checkbox: {
    height: 24,
    width: 24,
    borderRadius: 4,
    borderWidth: 2,
    borderColor: Color.neutral3,
    alignItems: 'center',
    justifyContent: 'center',
  },
  checked: {
    backgroundColor: Color.primary700,
    borderColor: Color.primary700,
  },
  disabled: {
    borderColor: Color.neutral3,
    backgroundColor: Color.neutral3,
  },
  checkedAndDisabled: {
    backgroundColor: Color.primary100,
    borderColor: Color.primary100,
  },
  label: {
    marginLeft: 8,
  },
});

이런 식으로 스타일링을 작성했습니다.

 

완성된 UI 화면은 이렇습니다. 이로써 저는 디자인 시스템의 checkbox와 동일하게 구현을 했습니다.


개선

UX

 

현재 코드 상으로 위의 완성된 checkbox의 check라는 Label을 누른다면 check가 되지 않습니다. 그 이유는 Press Event가 존재하지 않기 때문입니다.
하지만 사용자들은 checkbox를 사용할 때 체크박스를 정확히 눌러야 하는 UX보다는 label을 눌러도 check가 활성화가 되는 UX가 더 편하고 익숙합니다.
그에 맞춰서 개선 작업을 진행해 보겠습니다.

방법1

children 이 있을 때 렌더링 하는 태그를 View에서 Preassble 태그로 변경 후 onPress에 triger event를 구현하는 방법입니다.

//추가 함수
const triggerCheckbox = () => {
	if (!disabled) {
    	onPressedHandler();
    }
};

//변경 전
{children && (
	<View style={styles.label} onPress={triggerCheckbox}>
		{children}
	</View>
)}

//변경 후
{children && (
	<Pressable style={styles.label} onPress={triggerCheckbox}>
		{children}
	</Pressable>
)}

이런 식으로 label 영역 자체도 press가 가능하도록 변경하고 triggerCheckbox라는 함수를 통해서 마치 체크박스가 눌린 듯이 동작하도록 구현할 수 있습니다.

 

방법2

가장 최상위의 엘리먼트 요소를 View에서 Pressable로 변경하는 방법도 있습니다.

<Pressable style={[styles.container, style]}  
disabled={disabled} 
onPress={onPressedHandler}
>
//...child
</Pressable>

주의사항은 onPrees, disabled를 꼭옮겨주셔야 합니다.

설계

 

제가 개발하고 있는 환경에서는 지금으로서는 checkbox의 size가 24로 고정이 되어 있지 않지만 그렇지 않은 분들 같은 경우 props의 size를 추가해도 좋을 거 같습니다.

 

props에 추가

size: 'small' | 'medium' | 'large';

size 단위 별로 값 설정

const SIZE = {
  small: 16,
  medium: 24,
  large: 32,
};

이런 식으로 사용할 수 있을 거 같습니다!

style={
	{
		width: SIZE[size],
		height: SIZE[size],
	}
}

주로 사용하게 될 사이즈가 medium이라면 props의 size를?를 사용하여 옵셔널 처리로 변경하고
defaultProps를 사용하면 좋을 것 같습니다.

UiCheckbox.defaultProps = {
  size: 'medium',
};

후기

이로써 저는 TODO로 남겨놨던 작업을 해결했습니다.

 

제 생각에 프론트엔드 개발에서 실력을 보여줄 수 있는 부분 중 하나는 공통 컴포넌트를 얼마나 유연하게 구현하느냐 어느 정도의 포인트는 있다고 생각합니다.

그래서 저는 공통 컴포넌트를 만들 때 이것이 사용될 상황을 상상을 하고는 구현을 합니다 여기서는 이 컴포넌트가 이런 식으로 사용될 것이다 하면서 그런 뒤 사용 용도를 바탕으로 컴포넌트가 해줘야 할 역할을 추리다 보면 제 나름의 생각으로 재사용이 완전히 불가능한 컴포넌트가 나오지는 않는 거 같습니다.

다른 분들의 컴포넌트 설계 구현 팁이 있다면 언제든지 공유해 주시면 감사하겠습니다

'개발' 카테고리의 다른 글

useDebounce를 활용하기  (1) 2023.06.07
React Native Chip을 구현해보자  (0) 2023.05.26
내 입맛에 맞게 useList를 구현하기  (0) 2023.05.10
React Native useModal 구현하기  (0) 2023.05.02
useCodePush 구현하기  (3) 2023.04.24