본문 바로가기
SPA/React

[React] react-hook-form에서 Cannot read properties of undefined (reading '_f') 발생되는 경우

by J4J 2023. 2. 1.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 react-hook-form Cannot read properties of undefined (reading '_f') 에러에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

발생되는 경우

 

먼저 다음의 간단한 코드를 봐보도록 하겠습니다.

 

import React, { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import * as yup from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';

interface UseForm {
    firstPhone: string;
    secondPhone: string;
    thirdPhone: string;
}

const App = () => {

    /**
     * validation
     */
    const schema = yup.object().shape({
        firstPhone: yup.string().nullable().required('전화번호를 입력해주세요.'),
        secondPhone: yup.string().nullable().required('전화번호를 입력해주세요.'),
        thirdPhone: yup.string().nullable().required('전화번호를 입력해주세요.'),
    })

    const { register, setFocus, watch, getValues } = useForm<UseForm>({
        resolver: yupResolver(schema),
    })

    /**
     * useEffect
     */
    useEffect(() => {
        if(getValues('firstPhone').length >= 3) { // 첫번째 전화번호의 길이가 3이 넘어가는 경우 두번째 전화번호로 포커싱
            setFocus('secondPhone');
        }
    }, [watch('firstPhone')])

    useEffect(() => {
        if(getValues('secondPhone').length >= 4) { // 두번째 전화번호의 길이가 4를 넘어가는 경우 세번째 전화번호로 포커싱
            setFocus('thirdPhone');
        }
    }, [watch('secondPhone')])

    return (
        <div>
            <h2>
                전화번호 입력 기준치가 되면 다음 입력칸으로 넘어가는 테스트
            </h2>

            <div style={{display: 'flex', gap: '12px'}}>
                <input type="text" maxLength={3} placeholder="첫번째" {...register('firstPhone')} />
                <input type="text" maxLength={4} placeholder="두번째" {...register('secondPhone')} />
                <input type="text" maxLength={4} placeholder="세번째" {...register('thirdPhone')} />
            </div>
        </div>
    )
}

export default App;

 

 

 

위의 코드는 react-hook-form을 활용하여 유효성을 체크하면서 사용자들의 전화번호를 입력하는 예시 코드입니다.

 

전화번호 입력을 "-"를 기준으로 3개의 input 파트로 나눈 뒤 입력을 할 때 사용자들의 편의성을 위해 앞선 전화번호들이 어느 기준을 넘어가는 순간 다음 전화번호 입력 input으로 자동 포커싱이 되는 것을 구현하려고 합니다.

 

그리고 우선 위의 코드는 에러가 발생되지 않고 정상적으로 동작됩니다.

 

 

 

하지만 상황에 따라 데이터를 불러와 초기 데이터를 적재할 수도 있으니 다음과 같이 마운트가 처음 되었을 때 react-hook-form의 reset 함수를 이용하여 일방적으로 데이터를 넣어보려고 합니다.

 

import React, { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import * as yup from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';

interface UseForm {
    firstPhone: string;
    secondPhone: string;
    thirdPhone: string;
}

const App = () => {

    /**
     * validation
     */
    const schema = yup.object().shape({
        firstPhone: yup.string().nullable().required('전화번호를 입력해주세요.'),
        secondPhone: yup.string().nullable().required('전화번호를 입력해주세요.'),
        thirdPhone: yup.string().nullable().required('전화번호를 입력해주세요.'),
    })

    const { register, setFocus, watch, getValues, reset } = useForm<UseForm>({
        resolver: yupResolver(schema),
    })

    /**
     * useEffect
     */
    useEffect(() => {
        reset({
            firstPhone: '010',
            secondPhone: '1234',
            thirdPhone: '56'
        });
    }, [])

    useEffect(() => {
        if(getValues('firstPhone').length >= 3) { // 첫번째 전화번호의 길이가 3이 넘어가는 경우 두번째 전화번호로 포커싱
            setFocus('secondPhone');
        }
    }, [watch('firstPhone')])

    useEffect(() => {
        if(getValues('secondPhone').length >= 4) { // 두번째 전화번호의 길이가 4를 넘어가는 경우 세번째 전화번호로 포커싱
            setFocus('thirdPhone');
        }
    }, [watch('secondPhone')])

    return (
        <div>
            <h2>
                전화번호 입력 기준치가 되면 다음 입력칸으로 넘어가는 테스트
            </h2>

            <div style={{display: 'flex', gap: '12px'}}>
                <input type="text" maxLength={3} placeholder="첫번째" {...register('firstPhone')} />
                <input type="text" maxLength={4} placeholder="두번째" {...register('secondPhone')} />
                <input type="text" maxLength={4} placeholder="세번째" {...register('thirdPhone')} />
            </div>
        </div>
    )
}

export default App;

 

 

반응형

 

 

그러면 다음과 같은 에러가 발생됩니다.

 

setFocus 에러

 

 

 

문제해결 방법

 

먼저 위의 에러가 발생되는 원인은 reset으로 초기 데이터를 넣어줄 때 useEffect 내부에서 setFocus가 실행되면 발생되는 것으로 보입니다.

 

그래서 만약 reset을 이용해 데이터를 적재할 때 다음과 같이 if문 내부를 실행되지 않게하여 setFocus가 실행되는 케이스를 만들어주지 않으면 에러가 발생되지 않습니다.

 

useEffect(() => {
    reset({
        firstPhone: '01',
        secondPhone: '123',
        thirdPhone: '56'
    });
}, [])

 

 

 

그리고 이를 해결할 수 있는 방법은 제가 아는 것은 두 가지가 있습니다.

 

 

 

[ 1. reset 대신 setValue 사용 ]

 

먼저 코드를 보겠습니다.

 

import React, { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import * as yup from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';

interface UseForm {
    firstPhone: string;
    secondPhone: string;
    thirdPhone: string;
}

const App = () => {

    /**
     * validation
     */
    const schema = yup.object().shape({
        firstPhone: yup.string().nullable().required('전화번호를 입력해주세요.'),
        secondPhone: yup.string().nullable().required('전화번호를 입력해주세요.'),
        thirdPhone: yup.string().nullable().required('전화번호를 입력해주세요.'),
    })

    const { register, setFocus, watch, getValues, setValue } = useForm<UseForm>({
        resolver: yupResolver(schema),
    })

    /**
     * useEffect
     */
    useEffect(() => {
        // reset에서 setValue로 변경
        setValue('firstPhone', '010');
        setValue('secondPhone', '1234');
        setValue('thirdPhone', '56');
    }, [])

    useEffect(() => {
        if(getValues('firstPhone').length >= 3) { // 첫번째 전화번호의 길이가 3이 넘어가는 경우 두번째 전화번호로 포커싱
            setFocus('secondPhone');
        }
    }, [watch('firstPhone')])

    useEffect(() => {
        if(getValues('secondPhone').length >= 4) { // 두번째 전화번호의 길이가 4를 넘어가는 경우 세번째 전화번호로 포커싱
            setFocus('thirdPhone');
        }
    }, [watch('secondPhone')])

    return (
        <div>
            <h2>
                전화번호 입력 기준치가 되면 다음 입력칸으로 넘어가는 테스트
            </h2>

            <div style={{display: 'flex', gap: '12px'}}>
                <input type="text" maxLength={3} placeholder="첫번째" {...register('firstPhone')} />
                <input type="text" maxLength={4} placeholder="두번째" {...register('secondPhone')} />
                <input type="text" maxLength={4} placeholder="세번째" {...register('thirdPhone')} />
            </div>
        </div>
    )
}

export default App;

 

 

 

기존 코드에서 reset을 사용하던 부분을 위의 코드처럼 setValue로 수정하게 되면 에러가 발생되지 않는 것을 볼 수 있습니다.

 

하지만 일반적으로 API로부터 전달받은 데이터를 적재할 때 객체형태로 되어 있기 때문에 setValue를 이용하는 것은 아름답지 않은 상황이 발생할 수 있을 것으로 생각합니다.

 

그럴 때는 다음의 방법을 이용하여 reset을 그대로 사용해 주시면 될 것 같습니다.

 

 

728x90

 

 

[ 2. setTimeout 활용 ]

 

두 번째도 코드를 먼저 보겠습니다.

 

import React, { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import * as yup from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';

interface UseForm {
    firstPhone: string;
    secondPhone: string;
    thirdPhone: string;
}

const App = () => {

    /**
     * validation
     */
    const schema = yup.object().shape({
        firstPhone: yup.string().nullable().required('전화번호를 입력해주세요.'),
        secondPhone: yup.string().nullable().required('전화번호를 입력해주세요.'),
        thirdPhone: yup.string().nullable().required('전화번호를 입력해주세요.'),
    })

    const { register, setFocus, watch, getValues, reset } = useForm<UseForm>({
        resolver: yupResolver(schema),
    })

    /**
     * useEffect
     */
    useEffect(() => {
        reset({
            firstPhone: '010',
            secondPhone: '1234',
            thirdPhone: '56'
        });
    }, [])

    useEffect(() => {
        if(getValues('firstPhone').length >= 3) { // 첫번째 전화번호의 길이가 3이 넘어가는 경우 두번째 전화번호로 포커싱
            // setTimeout을 이용하여 코드 동작 시점을 변경
            setTimeout(() => {
                setFocus('secondPhone');
            }, 0)
        }
    }, [watch('firstPhone')])

    useEffect(() => {
        if(getValues('secondPhone').length >= 4) { // 두번째 전화번호의 길이가 4를 넘어가는 경우 세번째 전화번호로 포커싱
            // setTimeout을 이용하여 코드 동작 시점을 변경
            setTimeout(() => {
                setFocus('thirdPhone');
            }, 0)
        }
    }, [watch('secondPhone')])

    return (
        <div>
            <h2>
                전화번호 입력 기준치가 되면 다음 입력칸으로 넘어가는 테스트
            </h2>

            <div style={{display: 'flex', gap: '12px'}}>
                <input type="text" maxLength={3} placeholder="첫번째" {...register('firstPhone')} />
                <input type="text" maxLength={4} placeholder="두번째" {...register('secondPhone')} />
                <input type="text" maxLength={4} placeholder="세번째" {...register('thirdPhone')} />
            </div>
        </div>
    )
}

export default App;

 

 

 

위의 코드는 useEffect내부 setFocus를 활용하는 부분에 setTimeout을 0초로 하여 덮어주는 것입니다.

 

이 원리에 대해 이해를 하기 위해서는 자바스크립트의 비동기처리 방식과 관련된 이벤트루프 개념에 대해 먼저 아실 필요가 있습니다.

 

간단하게 설명드리면 자바스크립트는 싱글 스레드로 동작되는 언어이기 때문에 동기처리를 수행하는 도중 비동기처리 결과가 전달될 경우 결과 내용을 이벤트큐에 보관하고 있다가 동기처리 수행이 완료되면 이벤트큐에 보관되어 있는 비동기처리를 수행합니다.

 

즉, setTimeout은 비동기처리로 수행되기 때문에 useEffect 내부에 setFocus와 관련된 코드가 수행되는 시점을 가장 마지막으로 변경해 준 것입니다.

 

 

 

사실 왜 이 코드가 왜 정상적으로 동작되는지에 대해서는 정확하게는 모릅니다.

 

단지 문제를 접하신 개발자분들에게 조금이라도 도움이 될 것이라고 생각되어 관련 내용을 작성합니다.

 

 

 

 

 

 

이상으로 react-hook-form Cannot read properties of undefined (reading '_f') 에러에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글