온라인 강의/React 완벽 가이드 [Udemy]

138. Forward Refs"에 대해 알아보기

유호야 2023. 6. 14. 18:53
반응형

이번에 보여줄 훅은 Input 컴포넌트와 명령형으로 상호 작용할 수 있게 해준다

즉, 예를 들어 어떤 state를 전달해서 그 컴포넌트에서 무언가를 변경하는 방식이 아니라

컴포넌트 내부에서 함수를 호출하는 방식으로 이용한다. 

또 자주 할 필요도 없겠지만 자주 해서도 안 된다

 

이제는 Login 버튼을 활성화해서 유효하지 않은 값을 넣은 input 창에 효과를 주도록 하겠다.

useRef에 대해서 기억하는가?
값이 유효하지 않을 때, 이 개념을 사용할 것이다

 

 

115. "ref"로 작업하기

ref는 매우 강력한 도구로 가장 기본적인 기능은 다른 DOM 요소에 접근해서 그것들로 작업할 수 있게 해주는 것이다 폼을 제출할 때만 필요한데도 키를 입력할 때마다 state를 업데이트한다는 건

ninetynine-2026.tistory.com

 

 

ref prop은 Input 같은 모든 내장 HTML 컴포넌트에서 지원된다

아래와 같이 내장 객체를 이용, useEffect를 이용해서 password에 focus를 주었지만 우리가 원하는 것은 이게 아니다

import React, { useRef, useEffect } from 'react';
import classes from './input.module.css';

const Input = (props) => {
  const inputRef = useRef();
  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <div
    className={`${classes.control} ${props.isValid === false ? classes.invalid : ''
      }`}>
    <label htmlFor={props.id}>{props.label}</label>
    <input
      ref={inputRef}
      type={props.type}
      id={props.id}
      value={props.value}
      onChange={props.onChange}
      onBlur={props.onBlur}
    />
  </div>
}
export default Input;

 

목표는 activate 함수를 Input 내부가 아니라 외부에서 호출하려는 것이다
> 흔치 않은 경우다 

 

Login.js 로 가서

const emailInputRef = useRef();
const passwordInputRef = useRef();

const submitHandler = (event) => {
    event.preventDefault();

    if (formIsValid) {
      authCtx.onLogin(emailState.value, passwordState.value);
    } else if (!emailIsValid) {
      emailInputRef.current.focus();
    } else {
      passwordInputRef.current.focus();
    }
  };

하고 진행하면 될 줄 알았으나 안 된다!

 

 

그럴 때 useImperativeHandle 훅을 사용하면 컴포넌트나 컴포넌트 내부에서 오는 기능들을 명령적으로 사용할 수 있게 해준다

 

이 훅은 자주 사용하는 훅이 아니다. 

useImperativeHandle 및 forwardRef를 사용하면

리액트 컴포넌트에서 온 기능을 노출하여 부모 컴포넌트에 연결한 다음, 부모 컴포넌트 안에서 참조를 통해 그 컴포넌트를 사용하고 기능을 Trigger 할 수 있다. 

 

그러나 이 기능은 가능하면 반드시 피하는 것이 좋지만 특히 이와 같이 포커스나

스크롤링같은 다른 사용 사례에서는 매우 유용할 수 있다


Input.js

import React, { useRef, useImperativeHandle } from 'react';
import classes from './input.module.css';

const Input =
  React.forwardRef((props, ref) => {
    const inputRef = useRef();
    const activate = () => {
      inputRef.current.focus();
    }

    useImperativeHandle(ref, () => {
      return {
        focus: activate
      }
    });

    return <div
      className={`${classes.control} ${props.isValid === false ? classes.invalid : ''
        }`}>
      <label htmlFor={props.id}>{props.label}</label>
      <input
        ref={inputRef}
        type={props.type}
        id={props.id}
        value={props.value}
        onChange={props.onChange}
        onBlur={props.onBlur}
      />
    </div>
  }

  );
export default Input;

Login.js

import React, { useState, useEffect, useReducer, useContext, useRef } from 'react';

import Card from '../UI/Card/Card';
import classes from './Login.module.css';
import Button from '../UI/Button/Button';
import AuthContext from '../store/auth-context';
import Input from '../UI/Input/Input';
const Login = () => {
  const authCtx = useContext(AuthContext);

  // const [enteredEmail, setEnteredEmail] = useState('');
  // const [emailIsValid, setEmailIsValid] = useState();
  // const [enteredPassword, setEnteredPassword] = useState('');
  // const [passwordIsValid, setPasswordIsValid] = useState();
  const [formIsValid, setFormIsValid] = useState(false);

  const emailReducer = (state, action) => {
    if (action.type === 'USER_INPUT') {
      return { value: action.val, isValid: action.val.includes('@') }
    } else if (action.type === 'USER_BLUR') {
      return { value: state.value, isValid: state.value.includes('@') }
    }
    return { value: '', isValid: false };
  }

  const passwordReducer = (state, action) => {
    if (action.type === 'PWD_INPUT') {
      return { value: action.val, isValid: action.val.trim().length > 6 };
    } else if (action.type === 'PWD_BLUR') {
      return { value: state.value, isValid: state.value.trim().length > 6 };
    }
    return { value: state.value, isValid: false };
  }

  const [emailState, dispatchEmail] = useReducer(emailReducer, { value: '', isValid: null });
  const [passwordState, dispatchPassword] = useReducer(passwordReducer, { value: '', isValid: null });

  const emailInputRef = useRef();
  const passwordInputRef = useRef();

  useEffect(() => {
    console.log('EFFECT RUNNING');

    return () => {
      console.log('EFFECT CLEANUP');
    };
  }, []);

  const { isValid: emailIsValid } = emailState;
  const { isValid: passwordIsValid } = passwordState;

  useEffect(() => {
    const identifier = setTimeout(() => {
      console.log('Checking form validity!');
      setFormIsValid(
        emailIsValid && passwordIsValid
      );
    }, 500);

    return () => {
      console.log('CLEANUP');
      clearTimeout(identifier);
    };
  }, [emailIsValid, passwordIsValid]);

  const emailChangeHandler = (event) => {
    // setEnteredEmail(event.target.value);
    dispatchEmail({ type: 'USER_INPUT', val: event.target.value })

    setFormIsValid(
      emailState.isValid && passwordState.isValid
    );
  };

  const passwordChangeHandler = (event) => {
    dispatchPassword({ type: 'PWD_INPUT', val: event.target.value })
    // setEnteredPassword(event.target.value);

    setFormIsValid(
      emailState.isValid && event.target.value.trim().length > 6
    );
  };

  const validateEmailHandler = () => {
    // setEmailIsValid(emailState.isValid);
    dispatchEmail({ type: 'USER_BLUR' });
  };

  const validatePasswordHandler = () => {
    // setPasswordIsValid(enteredPassword.trim().length > 6);
    dispatchPassword({ type: 'PWD_BLUR' });

  };

  const submitHandler = (event) => {
    event.preventDefault();

    if (formIsValid) {
      authCtx.onLogin(emailState.value, passwordState.value);
    } else if (!emailIsValid) {
      emailInputRef.current.focus();
    } else {
      passwordInputRef.current.focus();
    }
  };

  return (
    <Card className={classes.login}>
      <form onSubmit={submitHandler}>
        <Input ref={emailInputRef} id="email" label="E-Mail" type="email" isValid={emailIsValid} value={emailState.value} onChange={emailChangeHandler} onBlur={validateEmailHandler} />
        <Input ref={passwordInputRef} id="password" label="Password" type="password" isValid={passwordIsValid} value={passwordState.value} onChange={passwordChangeHandler} onBlur={validatePasswordHandler} />

        <div className={classes.actions}>
          <Button type="submit" className={classes.btn}>
            Login
          </Button>
        </div>
      </form>
    </Card>
  );
};

export default Login;

 

반응형