[React] ref 으로 작업하기

SuamKang·2023년 7월 7일
0

React

목록 보기
16/34
post-thumbnail

ref는 참조라는 뜻으로 'reference'를 줄여 표현했다.

리액트에서 ref는 DOM 요소나 클래스 컴포넌트 인스턴스에 접근하는 방법을 제공하는 기능이다. ref를 사용하면 해당 요소나 컴포넌트의 속성과 메서드에 직접적으로 접근할 수 있다.

사용되는 가장 일반적인 경우는 2가지이다.

  1. DOM 요소에 접근하기: ref를 사용하여 특정 DOM 요소에 접근하고 해당 요소의 속성이나 메서드를 조작한다.
  1. 컴포넌트 인스턴스에 접근하기: ref를 사용하여 클래스 컴포넌트의 인스턴스에 접근하고 해당 인스턴스의 메서드를 호출하거나 속성을 조작한다.

보통 input요소에서 사용자의 입력값을 받아 업데이트 할때 state를 관리하게 되는데,
폼 제출하게 될때만 state를 업데이트 해주는게 좋지, 키를 입력할때마다 state를 업데이트한다는건 좀 불필요하지 않을까싶다.
이때 ref를 사용하며
useRef()을 사용하여 설정할 수 있다.

그렇다면 useRef는 무엇을 반환하고 어떤값을 취하게 되는걸까?

우선 초기화하려는 기본값이 필요하지만, 보통은 해당 ref와 작업할 수 있게해주는 연결해주는 값을 반환해주길 원한다.
즉, 요소에 연결해 해당 요소와 작업할 수 있게 해주는것이다.


  const nameInputRef = useRef();
  const ageInputRef = useRef();

어떠한 html요소라도 우리가 만든 ref중 하나에 연결 가능하다. 예를들면 사용자 입력데이터를 접근해서 가져오고 싶은 input요소에 자주 일어난다.

만약 저 nameInputRef로 지정한 ref를 현재 사용자 이름값을 받는 input요소에 연결시켜놓았다면, 사용자가 입력한 이름값이 저장될테고, 그걸 기반으로 그 값은 실제 DOM요소가 된다.
ref는 항상 객체이고 그 프로퍼티로 current값을 가진다.
이는 항상 ref가 연결된 실제 값을 갖는다. 해당 프로퍼티에 저장되어있는 것들은 다 실제 dom 노드이다.

기본값은 현재 없지만
해당 jsx코드가 렌더링될때 ref프롭때문에 'nameInputRef'는 input에 연결된다.
따라서 여기선

실제값 = input

이다.

하지만 사실 직접조작하지 않는게 좋긴하다, 왜냐하면 리액트를 애초에 사용하는 이유가 번거로운 작업들을 위해서 사용하는거기 때문에 DOM은 리액트에 의해서만 조작되어야하기 때문이다.

하지만 해당 작업이 위와같이 인풋의 데이터를 읽는것과 같은 단순한것은 괜찮다.

예시

기존 useState훅을 이용해서 관리하던 이름과 나이 state를 useRef훅을 이용해서 대체해보았다.

Before

function AddUserForm(props) {
  const [userName, setUserName] = useState("");
  const [userAge, setUserAge] = useState("");
  
  
  const nameChangeHandler = (event) => {
    setUserName(event.target.value);
  };

  const ageChangeHandler = (event) => {
    setUserAge(event.target.value);
  };

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

    if ((userName.trim().length && userAge.trim().length) === 0) {
      setError({
        title: "Invalid input",
        message: "Please enter a valid name and age(non-empty values).",
      });
      return;
    }
    if (Number(userAge.trim()) < 1) {
      setError({
        title: "Invalid age",
        message: "Please enter a valid age (> 0).",
      });
      setUserName("");
      setUserAge("");
      return;
    }

    const newUserInfo = {
      id: Math.random().toString(),
      name: userName,
      age: userAge,
    };

    props.onAddItem(newUserInfo);
    setUserName("");
    setUserAge("");
  };
  
  return (
    <Wrapper>
      {error && (
        <Modal
          title={error.title}
          message={error.message}
          onToggle={errorHandler}
        />
      )}
      <Card className={styles.container}>
        <form onSubmit={formSubmitHandler}>
          <div className={styles["form-userName"]}>
            <label htmlFor="username">Username</label>
            <input
              type="text"
              id="username"
              value={userName}
              onChange={nameChangeHandler}
            />
          </div>
          <div className={styles["form-age"]}>
            <label htmlFor="age">Age (Years)</label>
            <input
              type="number"
              id="age"
              value={userAge}
              onChange={ageChangeHandler}
            />
          </div>
          <Button type="submit">Add User</Button>
        </form>
      </Card>
    </Wrapper>
  );
}

export default AddUserForm;

After

function AddUserForm(props) {
  const nameInputRef = useRef();
  const ageInputRef = useRef();

  const [error, setError] = useState();


  const formSubmitHandler = (event) => {
    event.preventDefault();
    // ref를 이용한 해당 input dom요소에 직접 접근하여 value를 가져옴
    const enteredName = nameInputRef.current.value;
    const enteredAge = ageInputRef.current.value;

    if ((enteredName.trim().length && enteredAge.trim().length) === 0) {
      setError({
        title: "Invalid input",
        message: "Please enter a valid name and age(non-empty values).",
      });
      return;
    }
    if (Number(enteredAge.trim()) < 1) {
      setError({
        title: "Invalid age",
        message: "Please enter a valid age (> 0).",
      });
      nameInputRef.current.value = ''; // 인풋값 재설정
      ageInputRef.current.value = '';
      return;
    }

    const newUserInfo = {
      id: Math.random().toString(),
      name: enteredName,
      age: enteredAge,
    };

    props.onAddItem(newUserInfo);
    nameInputRef.current.value = ''; // 인풋값 재설정
    ageInputRef.current.value = '';
  };

  const errorHandler = () => {
    setError(null);
  };

  return (
    <Wrapper>
      {error && (
        <Modal
          title={error.title}
          message={error.message}
          onToggle={errorHandler}
        />
      )}
      <Card className={styles.container}>
        <form onSubmit={formSubmitHandler}>
          <div className={styles["form-userName"]}>
            <label htmlFor="username">Username</label>
            <input
              type="text"
              id="username"
              ref={nameInputRef}
            />
          </div>
          <div className={styles["form-age"]}>
            <label htmlFor="age">Age (Years)</label>
            <input
              type="number"
              id="age"
              ref={ageInputRef}
            />
          </div>
          <Button type="submit">Add User</Button>
        </form>
      </Card>
    </Wrapper>
  );
}

export default AddUserForm;

이전코드는 state기반 해결법으로 사용되었고, ==> 제어되는 컴포넌트 방식
이후코드는 ref를 적용한 코드이다. ==> 제어되지 않는 컴포넌트 방식

앞서 얘기했다시피 이걸 적용해보면 좋을땐 단순히 값만 빠르게 읽고 판단하고 싶을 경우와 아무것도 바꿀 계획이 없다면 state관리는 그리 필요 없게 될거다.

state를 키 로그 기록용으로 사용하는건 별로 좋지 않기 때문이다.
왜냐하면 코드가 길어지고 다양한 기능들이 추가가 되면 불필요한 코드와 작업이 많아지게 될뿐이기 때문이다.

정리하자면

ref는 는 코드가 적지만 DOM을 직접 접근해 조작한다는 예외적인 일을 해야하고,
state 는 확실히 더 깔끔하지만 코드를 더 많이 작성되어야 한다는 점이 있겠다.

사실 ref를 사용하면 편하게 요소에 접근 가능하기 때문에 많은 리액트 프로젝트에서 ref가 좀더 많이 사용되는 비중이 있다.


📍React.forwardRef()


ref를 사용하여 직접 DOM에 접근하여 조작할 수 있는방법으로 헨들링 하다보면, 자바스크립트의 네이티브 요소가 아닌 사용자 정의 컴포넌트로 ref를 적용하게 되는 경우도 생기게 된다.

예를들면 Input이라는 재사용 가능한 컴포넌트를 제작했다고 봤을때,
이 Input이라는건 일반 요소가 아닌 컴포넌트 자체 함수이기에 해당 속성으로 ref를 적용한다해도 먹히지 않는다.

ref 속성이 적용이 되게하기 위해선 해당 ref속성을 받고싶은 컴포넌트(여기선 Input)에 가서 React객체를 import 한 후에 React.forwardRef()로 감싸주면 된다.

React.forwardRef((props, ref) => {})

해당 첫번째 인자로는 해당 컴포넌트 함수가 들어가고, 두번째 인자로 ref를 적용해주면 해당 ref속성이 전달받아지는 연결고리가 되어 컴포넌트 안에서 사용 가능해진다.


아래는 적용한 Input 컴포넌트 예시이다.

import React from "react";

import classes from "./Input.module.css";


const Input = React.forwardRef((props, ref) => {
  return (
    <div className={classes.input} style={props.style}>
      <label htmlFor={props.input.id}>{props.label}</label>
      // 여기서 사용!
      <input ref={ref} {...props.input} />
    </div>
  );
});

export default Input;

이렇게 해서 ref를 통해 인풋요소에 접근할 수 있게 된다.
이것 또한 리액트에서 제공하는 메소드이며 이를 활용하여 사용자 정의 컴포넌트로 만들어진 요소에 ref를 적용하여 접근해 사용할 수 있게 되었다.

profile
마라토너같은 개발자가 되어보자

0개의 댓글