React 6자리 인증코드 컴포넌트

박창준·2023년 7월 31일

react

목록 보기
1/1
post-thumbnail

요구사항

  1. 6자리 번호를 입력받는다
  2. 번호별로 입력칸이 달라야 한다
  3. 한 칸에 입력하면 다음 칸으로 focus가 옮겨가야 한다
  4. React Native로 구현해야한다

구현 안(못)한 요구사항

  1. 무조건 숫자만 들어가야한다
  2. 일반적인 텍스트입력과 완전히 같은 UX를 보여줘야한다

구현내용

import React from 'react';
import {
  Pressable,
  PressableProps,
  StyleSheet,
  TextInput,
  TextInputProps,
} from 'react-native';

const styles = StyleSheet.create({
  container: {
    width: 44,
    height: 44,
    borderRadius: 12,
    backgroundColor: '#E7ECF9',
    justifyContent: 'center',
  },
  textInput: {
    fontSize: 16,
    textAlign: 'center',
  },
});

export type SingleDigitInputProps = {
  onChangeText: NonNullable<TextInputProps['onChangeText']>;
  value: NonNullable<TextInputProps['value']>;
};

export type SingleDigitInputRef = {
  focus: () => void;
  blur: () => void;
};

export default React.forwardRef<
  SingleDigitInputRef,
  SingleDigitInputProps
>(function SingleDigitInput(props, ref) {
  const inputRef = React.useRef<TextInput>(null);

  React.useImperativeHandle(
    ref,
    () => ({
      focus: () => {
        inputRef.current?.focus();
      },
      blur: () => {
        inputRef.current?.blur();
      },
    }),
    [],
  );
  const handlePress: NonNullable<PressableProps['onPress']> =
    React.useCallback(() => {
      inputRef.current?.focus();
    }, []);

  return (
    <Pressable style={styles.container} onPress={handlePress}>
      <TextInput
        ref={inputRef}
        style={styles.textInput}
        value={props.value}
        onChangeText={props.onChangeText}
        maxLength={1}
      />
    </Pressable>
  );
});

이렇게 한 칸 입력을 만들고

import SingleDigitInput, {
  SingleDigitInputProps,
  SingleDigitInputRef,
} from './SingleDigitInput';
import React from 'react';
import {StyleSheet, View} from 'react-native';

const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignSelf: 'stretch',
  },
});

const MAX_DIGITS = 6

export default function MultipleDigitsInput() {
  const [singleDigits, setSingleDigits] = React.useState<string[]>([]);

  const addDigit = React.useCallback(
    (digit: string) => {
      if (digit.length > 1) {
        return;
      }
      setSingleDigits(prev => {
        if (prev.length >= MAX_DIGITS) {
          return prev;
        }

        return [...prev, digit];
      });
    },
    [setSingleDigits],
  );

  const removeDigit = React.useCallback(
    ({index}: {index: number}) => {
      setSingleDigits(prev => {
        if (prev.length === 0) {
          return prev;
        }

        return [...prev.slice(0, index), ...prev.slice(index + 1)];
      });
    },
    [setSingleDigits],
  );

  const handleChangeSingleDigit: (
    index: number,
  ) => SingleDigitInputProps['onChangeText'] = React.useCallback(
    index => text => {
      if (text.length > 1 && index < MAX_DIGITS - 1) {
        refs.current[index + 1]?.focus();
        return;
      }

      if (text.length === 1 && index < MAX_DIGITS - 1) {
        addDigit(text);
        refs.current[index + 1]?.focus();
        return;
      }

      if (text.length === 1 && index === MAX_DIGITS - 1) {
        addDigit(text);
        return;
      }

      if (text.length === 0 && index > 0) {
        refs.current[index - 1]?.focus();
        removeDigit({index: index});
        return;
      }

      if (text.length === 0 && index === 0) {
        removeDigit({index: index});
        return;
      }
    },
    [addDigit, removeDigit],
  );

  const refs = React.useRef<SingleDigitInputRef[]>(
    Array.from({length: MAX_DIGITS}),
  );

  return (
    <View style={styles.container}>
      {Array.from({length: MAX_DIGITS}).map((_, index) => (
        <SingleDigitInput
          key={`${index}`}
          value={singleDigits[index] || ''}
          ref={r => r && (refs.current[index] = r)}
          onChangeText={handleChangeSingleDigit(index)}
        />
      ))}
    </View>
  );
}

이렇게 여러 칸 입력을 만든다

https://snack.expo.dev/@orlein/six-digits-input

결과물

고찰

  1. react-dom에서 사용 가능?
    -> 당연하다 대신 'react-native' dependency에 있는 컴포넌트는 알아서 잘 바꿀것...
  2. custom hook으로 만들기 가능?
    -> 당연하다 근데 굳이? 아 react-dom에서도 똑같이 사용하고싶다면야 뭐...
  3. 왜 React 팀은 forwardRef의 <T, P> 순서를 파라미터 순서에 맞춰 <P, T>로 하지 않았을까?
    -> 몰?루 괜히 헷갈리기만 하고 나쁘다... 하지만 또 이걸 좌우를 바꾸면 여기저기서 타이핑이 박살날듯
  4. useImperativeHandle라는걸 굳이 써야하나?
    -> ㅇㅇ ref라는걸 사용하는거 자체를 좀 지양할 필요는 있지만 이번꺼는 꼭 써줘야한다
    -> 게다가 무지막지한 원래의 ref를 그대로 오픈시키기보다, 구현에 따라 적당히 ref사용을 제한시킨다는 점에서 개발에 필요한 부분도 있다
  5. export default function .... 형태를 좋아하는 이유?
    -> 선언/export가 한 줄이라 이뻐서
    -> named function이 디버깅하기 편할때가 있어서
  6. StyleSheet 왜씀 딴거쓰지
    -> 딴거 생각하기 귀찮
profile
프론트를 주로하는 잡부

1개의 댓글

comment-user-thumbnail
2023년 7월 31일

잘 읽었습니다. 좋은 정보 감사드립니다.

답글 달기