useRef 사용으로 input 최적화하기 (리렌더링 막기)

준영·2023년 1월 8일
5
post-thumbnail

상황

부모 컴포넌트에 서버와 연결을 시도하는 로직이 존재하고, 그의 자식 컴포넌트로 인풋창이 있다.
부모컴포넌트에서 input의 state 값을 관리하고 자식컴포넌트로 부터 Input의 값과 set함수를 props로 받아와 onchange 이벤트로 값을 업데이트하는 방식이었다.

부모컴포넌트 (Chat.tsx)

🙋🏻‍♂️ 최대한 문제의 부분만 보여주기 위하여 생략된 코드들이 있으니 무시하고 봐주세요.


export default function Chat() {
  const client = mqtt.connect("mqtt서버에 연결 할 주소");
  const [inputValue, setInputValue] = useState("");

  useEffect(() => {
    console.log("클라이언트 접속 중..");
	
    const connectMqtt = () => {
      client.on("connect", () => {
        setPersonMessage([]);
        console.log("클라이언트 접속 완료!");
        // 해당 토픽 구독
        client.subscribe(roomName.channels, (err) => {
          if (!err) {
            console.log(roomName.channels + " 토픽 구독 성공!");
          } else {
            console.log("구독실패");
          }
        });
      });
    };

    if (client !== undefined) {
      connectMqtt();
    }
  }, [roomName]); // 채팅방이 바뀔 시, 클라이언트 연결

  // 해당 토픽으로 메세지 송신
  const sendFunc = () => {
    client.publish(
      roomName.channels,
      inputValue,
      { qos: 0, retain: false },
      function (error) {
        if (error) {
          console.log(error);
        } else {
          console.log("Published");
        }
      }
    );
  };

  return (
    <>
      <Send sendFunc={sendFunc} inputValue={inputValue} setInputValue={setInputValue} />
    </>
  );
}

자식컴포넌트(Send.tsx)

export default function Send({ sendFunc, inputValue, setInputValue }) {
  return (
    <SendBox>
      <SendInput ref={inputRef} onChange={(e) => {
        	setInputValue(e.target.value);
        }} value={inputValue} />
      <SendButton
        onClick={sendFunc}
      >
        보내기
      </SendButton>
    </SendBox>
  );
}

문제 (부모컴포넌트 리렌더링)

자식컴포넌트에서 값을 업데이트 할 때마다, 부모컴포넌트가 리렌더링이 일어나면서 서버에 연결을 자꾸 시도하는 문제가 발생했다.

문제의 화면

브로커 서버의 로그를 확인하면, onChange마다 서버와 연결을 시도하는 로그를 볼 수 있다.

조금만 타이핑을 많이하면 서버와 연결을 하는 횟수가 점점 늘어나더니, 메세지를 보내는 시간이 오래 걸리게 된다.


해결 (부모컴포넌트 리렌더링 해결)

useRef로 input의 값을 업데이트 하자

useState는 state가 변경되면 내부의 모든 변수들이 초기화 되는 반면에, useRef 컴포넌트가 리렌더링 되지않는다.

값을 입력 할때, 값을 저장만 할뿐 컴포넌트를 리렌더링 시키지않는다.

부모컴포넌트 (Chat.tsx)

🙋🏻‍♂️ 최대한 해결의 부분만 보여주기 위하여 생략된 코드들이 있으니 무시하고 봐주세요.

export default function Chat() {
  const client = mqtt.connect("mqtt서버에 연결 할 주소");
  const inputRef = useRef(null);

  useEffect(() => {
    console.log("클라이언트 접속 중..");
	
    const connectMqtt = () => {
      client.on("connect", () => {
        setPersonMessage([]);
        console.log("클라이언트 접속 완료!");
        // 해당 토픽 구독
        client.subscribe(roomName.channels, (err) => {
          if (!err) {
            console.log(roomName.channels + " 토픽 구독 성공!");
          } else {
            console.log("구독실패");
          }
        });
      });
    };

    if (client !== undefined) {
      connectMqtt();
    }
  }, [roomName]); // 채팅방이 바뀔 시, 클라이언트 연결

  // 해당 토픽으로 메세지 송신
  const sendFunc = () => {
    client.publish(
      roomName.channels,
      `[${userName}]: ${inputRef.current}`,
      { qos: 0, retain: false },
      function (error) {
        if (error) {
          console.log(error);
        } else {
          console.log("Published");
        }
      }
    );
  };

  return (
    <>
      <Send sendFunc={sendFunc} inputRef={inputRef} />
    </>
  );
}

자식컴포넌트(Send.tsx)

export default function Send({ sendFunc, inputRef }) {
  const onChange = (e) => {
    inputRef.current = e.target.value;
    console.log(inputRef.current);
  };

  return (
    <SendBox>
      <SendInput onChange={onChange}/>
      <SendButton
        onClick={sendFunc}
      >
        보내기
      </SendButton>
    </SendBox>
  );
}

부모컴포넌트 리렌더링을 해결한 화면

로그도 찍히지 않고, 리액트 개발툴로 확인해보면 input창에 리렌더링이 일어나지 않는 것을 볼 수 있다.


문제 (input의 초기화)

가장 원하는 문제는 해결했지만, state를 사용하지 않음으로 메세지를 보낸 뒤, input의 내용을 초기화 하지 못하는 문제가 생겼다. (바로 위의 영상만 봐도 알 수 있듯이..)


해결 (input의 초기화)

ref와 state를 둘다 사용하자

부모컴포넌트가 리렌더링되는 이유를 잘 생각해보면, props로 받은 state값이 업데이트가 되어서 리렌더링이 된 것이다.

하지만 그것을 우리는 ref를 사용하여 해결했지만, state를 사용하지 않고서는 input의 초기화가 불가능하다는 것도 알았다.

따라서 실질적인 보내는 값과 초기화를 위한 값을 따로 관리하면 되는것이다.

내가 말하고도 뭔 소린지 모르겠다. 그냥 코드를 보고 이해하자

자식컴포넌트(Send.tsx) <= 이것만 손보면 끝임

export default function Send({ sendFunc, inputRef }) {
  // 인풋값을 초기화를 위해서 자식 컴포넌트에서 따로 값을 관리할 state를 선언
  const [value, setValue] = useState("");

  const onChange = (e) => {
    // 실절적으로 보낼 값
    inputRef.current = e.target.value;
    console.log(inputRef.current);
    // 부모컴포넌트의 props데이터가 아니기 때문에, 부모컴포넌트까지 리렌더링이 발생하지 않음 (관리만 할 값임)
    setValue(inputRef.current);
  };

  // ref로는 초기화가 어렵기 때문에 따로 관리로 사용하려고 선언한 state값을 초기화 해줌
  const reset = () => {
    setValue("");
  };

  return (
    <SendBox>
      <SendInput ref={inputRef} onChange={onChange} value={value} />
      <SendButton
        onClick={() => {
          sendFunc();
          reset();
        }}
      >
        보내기
      </SendButton>
    </SendBox>
  );
}

실질적으로 보낼 값은 부모컴포넌트에서 ref를 받아와 current.value를 업데이트를 해주고,
인풋에 입력한 값을 보여주고 초기화만 하는 용도인 state값은 자식컴포넌트에서 선언하고 똑같이 값을 업데이트 시켜준다.
이렇게 하면 부모컴포넌트까지 리렌더링 되는 상황을 막을 수 있다. (input만 리렌더링이 일어날 것이다.)

인풋의 초기화와 부모컴포넌트의 리렌더링을 해결한 화면

리액트 개발자 도구로 확인해보면 input 컴포넌트만 리렌더링이 되는 것을 볼 수 있다.

profile
개인 이력, 포폴 관리 및 기술 블로그 사이트 👉 https://aimzero-web.vercel.app/

0개의 댓글