인프런 커뮤니티 질문&답변

HyeJung님의 프로필 이미지

작성한 질문수

맛집 지도앱 만들기 (React Native + NestJS)

[3-5] 폼 사용성 개선하기

ios 시뮬레이터에서 비밀번호(or 비밀번호 확인) 재 포커스 후 입력시 이전값이 초기화 됩니다

해결된 질문

24.06.01 04:28 작성

·

166

1

안드로이드에서는 초기화 문제 없는데 ios에서만 그러네요..ㅠ

일단 오류 확인에 있어 필요하다는 코드 위주로 올렸는데 더 필요한 코드가 있다면 알려주세요!

 

SignupScreen.tsx

import {SafeAreaView, StyleSheet, Text, TextInput, View} from 'react-native';
import React, {useRef} from 'react';
import InputField from '../../components/InputField';
import useForm from '../../hooks/useForm';
import CustomButton from '../../components/CustomButton';
import {validateSignup} from '../../utils';

function SignupScreen() {
  const passwordRef = useRef<TextInput | null>(null);
  const passwordConfirmRef = useRef<TextInput | null>(null);
  const signup = useForm({
    initialValue: {email: '', password: '', passwordConfirm: ''},
    validate: validateSignup,
  });

  const handleSubmit = () => {
    console.log(signup.values);
  };

  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.inputContainer}>
        <InputField
          autoFocus
          placeholder="이메일"
          error={signup.errors.email}
          touched={signup.touched.email}
          inputMode="email"
          returnKeyType="next" // return키가 아닌 다른 키 옵션을 주고 싶을때
          blurOnSubmit={false} // next와 같은 키를 눌러도 키가 닫히지 않음 (false)
          onSubmitEditing={() => passwordRef.current?.focus()} // next키를 눌렀을 때 다음 input으로 이동
          {...signup.getTextInputProps('email')}
        />
        <InputField
          key="password"
          ref={passwordRef}
          placeholder="비밀번호"
          textContentType="oneTimeCode" // ios에서 강력한 암호 뜨게 하는걸 방지
          error={signup.errors.password}
          touched={signup.touched.password}
          secureTextEntry
          returnKeyType="next"
          blurOnSubmit={false}
          onSubmitEditing={() => passwordConfirmRef.current?.focus()}
          {...signup.getTextInputProps('password')}
        />
        <InputField
          key="passwordConfirm"
          ref={passwordConfirmRef}
          placeholder="비밀번호 확인"
          textContentType="oneTimeCode"
          error={signup.errors.passwordConfirm}
          touched={signup.touched.passwordConfirm}
          secureTextEntry
          onSubmitEditing={handleSubmit}
          {...signup.getTextInputProps('passwordConfirm')}
        />
      </View>
      <CustomButton label="회원가입" />
    </SafeAreaView>
  );
}

export default SignupScreen;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    margin: 30,
  },
  inputContainer: {
    gap: 20,
    marginBottom: 30,
  },
});

useForm.ts

// 리액트 hook form 같은 리액트 라이브러리를 사용해도 괜찮지만
// 복잡하고 많은 input을 다루지 않기 때문에 직접 구현함
import {useEffect, useState} from 'react';

interface UseFormProps<T> {
  initialValue: T;
  validate: (values: T) => Record<keyof T, string>;
}

function useForm<T>({initialValue, validate}: UseFormProps<T>) {
  const [values, setValues] = useState(initialValue);
  const [touched, setTouched] = useState<Record<string, boolean>>({});
  const [errors, setErrors] = useState<Record<string, string>>({});

  const handleChangeText = (name: keyof T, text: string) => {
    setValues({
      ...values,
      [name]: text,
    });
  };

  const handleBlur = (name: keyof T) => {
    setTouched({
      ...touched,
      [name]: true,
    });
  };

  const getTextInputProps = (name: keyof T) => {
    const value = values[name];
    const onChangeText = (text: string) => handleChangeText(name, text);
    const onBlur = () => handleBlur(name);

    return {value, onChangeText, onBlur};
  };

  useEffect(() => {
    const newErrors = validate(values);
    setErrors(newErrors);
  }, [validate, values]);
  useEffect(() => {
    console.log('values값 뭐뭐 들어왔는지 체크용: ', values);
  }, [values]);

  return {values, errors, touched, getTextInputProps};
}

export default useForm;

validate.ts

type UserInformation = {
  email: string;
  password: string;
};

function validateUser(values: UserInformation) {
  const errors = {
    email: '',
    password: '',
  };

  // email이 올바른 이메일인지 검사하는 정규표현식
  // 이 테스트를 통과하지 못할경우
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
    errors.email = '올바른 이메일 형식이 아닙니다.';
  }
  if (!(values.password.length >= 8 && values.password.length < 20)) {
    errors.password = '비밀번호는 8~20자 사이로 입력해주세요.';
  }

  return errors;
}

function validateLogin(values: UserInformation) {
  return validateUser(values);
}

function validateSignup(values: UserInformation & {passwordConfirm: string}) {
  const errors = validateUser(values);
  const signupErrors = {...errors, passwordConfirm: ''};

  if (values.password !== values.passwordConfirm) {
    signupErrors.passwordConfirm = '비밀번호가 일치하지 않습니다.';
  }

  return signupErrors;
}

export {validateLogin, validateSignup};

InputField.tsx

import {
  Dimensions,
  Pressable,
  StyleSheet,
  Text,
  TextInput,
  TextInputProps,
  View,
} from 'react-native';
import React, {ForwardedRef, forwardRef, useRef} from 'react';
import {colors} from '../constants';
import {mergeRefs} from '../utils';

interface InputFieldProps extends TextInputProps {
  disabled?: boolean;
  error?: string;
  touched?: boolean;
}

// 디바이스의 높이 가져옴
const deviceHeight = Dimensions.get('screen').height;

const InputField = forwardRef(
  (
    {disabled = false, error, touched, ...props}: InputFieldProps,
    ref?: ForwardedRef<TextInput>,
  ) => {
    const inputRef = useRef<TextInput | null>(null);

    // input부분이 아닌 error msg(View컴포넌트) 부분을 클릭해도 input에 포커스를 해주기 위함 (Pressable 컴포넌트로 먼저 감싸주고나서!)
    const handlePressInput = () => {
      inputRef.current?.focus();
    };

    return (
      <Pressable onPress={handlePressInput}>
        <View
          style={[
            styles.container,
            disabled && styles.disabled,
            touched && Boolean(error) && styles.inputError,
          ]}>
          <TextInput
            ref={ref ? mergeRefs(inputRef, ref) : inputRef}
            // false일 때는 편집 가능
            editable={!disabled}
            placeholderTextColor={colors.GRAY_500}
            style={[styles.input, disabled && styles.disabled]}
            {...props}
            autoCapitalize="none" // 자동 대문자 방지
            spellCheck={false}
            autoCorrect={false}
          />
          {/* error 메세지가 있을 때만 해당 컴포넌트 표시하기 위해 string 타입을 Boolean으로 변경 */}
          {touched && Boolean(error) && (
            <Text style={styles.error}>{error}</Text>
          )}
        </View>
      </Pressable>
    );
  },
);

export default InputField;

const styles = StyleSheet.create({
  container: {
    borderWidth: 1,
    borderColor: colors.GRAY_200,
    padding: deviceHeight > 700 ? 15 : 10,
  },
  input: {
    fontSize: 16,
    color: colors.BLACK,
    padding: 0,
  },
  disabled: {
    backgroundColor: colors.GRAY_200,
    color: colors.GRAY_700,
  },
  inputError: {
    borderWidth: 1,
    borderColor: colors.RED_300,
  },
  error: {
    color: colors.RED_500,
    fontSize: 12,
    paddingTop: 5,
  },
});

common.ts

import {ForwardedRef} from 'react';

// input component를 만들어서 사용할 때 해당 컴포넌트에서 사용하는 ref와 외부에서 주입하는 ref를 둘 다 사용 가능하게 함
function mergeRefs<T>(...refs: ForwardedRef<T>[]) {
  return (node: T) => {
    refs.forEach(ref => {
      if (typeof ref === 'function') {
        ref(node);
      } else if (ref) {
        ref.current = node;
      }
    });
  };
}

export {mergeRefs};

 

찾아보기도 하고 챗gpt에도 물어보고 해봤지만 도저히 모르겠어서 질문 남겨봅니다!

일단 회원가입 스크린 관련해서만 올리긴 했는데 로그인 화면에서도 마찬가지로 비밀번호 부분만 그런 현상이 발생하네요..ㅠ

혹시나 해서 비밀번호 input 속성으로 secureTextEntry 전달하는거 없애보니까 이런 문제가 없긴한데 그렇다고 이걸 전달 안할 수도 없고..어찌해야 할까요?? 아니면 이건 코드가 아닌 ios 자체 문제일까요??

답변 1

0

Kyo님의 프로필 이미지
Kyo
지식공유자

2024. 06. 01. 04:40

안녕하세요. 비밀번호를 새로 입력할때 초기화 되는건 저도 그렇습니다. 비밀번호 특성상 그렇게 동작합니다.

HyeJung님의 프로필 이미지
HyeJung
질문자

2024. 06. 01. 04:47

답변 감사드립니다!! 기억상으로 다른 앱들에서 비밀번호 초기화가 안됐던 거 같아서 문제가 있나 싶었는데 아니었군요. 감사합니다!! 맘편히 다음 강의 들으러 가겠습니다

늦은 시간인데도 빠른 답변 너무 감사드려요 아직 초반인데도 꿀팁이나 이것저것 많이 얻어가는 것 같아서 너무 좋아요!! 질문글 남긴김에 적고 싶었던 말 적고가요 다시 한 번 감사드립니다🥺

Kyo님의 프로필 이미지
Kyo
지식공유자

2024. 06. 01. 04:51

네 ios textInput prop중에 clearTextOnFocus라는 옵션이 있는데 비밀번호에는 이를 false로 지정해도 무시되는것같네요. 다른 방법이 있는지 찾아봐야할것같아요.

많이 얻어간다니 좋네요! 저도 감사합니다 :)

Kyo님의 프로필 이미지
Kyo
지식공유자

2024. 06. 01. 06:21

좀더 찾아봤지만 secureTextEntry를 지정한상태에서 ios에서 해당 동작을 막기는 어려워보입니다.

이 기본적인 동작을 막고싶다면 네이티브 코드를 직접 수정하거나, secrueTextEntry를 지정하지않고 직접 '•' 문자로 대체하는 핸들러를 구현하는 방법이 있겠네요!

HyeJung님의 프로필 이미지
HyeJung
질문자

2024. 06. 01. 07:00

감사합니다!! 초기화 막고싶을 땐 알려주신 방법 참고해서 수정해봐야겠네요

HyeJung님의 프로필 이미지

작성한 질문수

질문하기