개발놀이터

컴포넌트 생명주기 (부제 : useEffect의 맹점) 본문

기타/리액트

컴포넌트 생명주기 (부제 : useEffect의 맹점)

마늘냄새폴폴 2024. 6. 18. 20:45

회사에서 리액트를 사용해서 어쩔 수 없이 리액트를 공부해야하는데 나름 재미는 있습니다. 근데 좀 그지같은 부분이 있더군요... 이번엔 포스팅에선 컴포넌트의 생명주기와 렌더링에 따른 useEffect의 호출 순서 그로인해 벌어지는 useEffect의 맹점에 대해서 알아보려고합니다. 

 

그지같은 리액트...

제가 원한건 useEffect에서 API를 호출하고 state에 저장한 뒤 state에 저장된 값을 HTML에 써먹고싶었습니다. 

 

기대했던 순서는 다음과 같습니다. 

 

  1. 리액트가 useEffect를 호출한다. 
  2. API가 호출된다. 
  3. 값이 가져와지고 그 값을 state에 저장한다. 
  4. 렌더링이 될 때 state 값을 사용한다. 

 

근데 이 그지같은 리액트는 제가 원하는대로 움직여주지 않더군요. 

 

  1. 렌더링이 되고 state값으로 HTML값을 세팅한다(?). useEffect가 호출되기 전이기 때문에 state엔 아무것도 없고 undefined가 찍힙니다. 
  2. useEffect를 호출한다. 
  3. API가 호출된다.
  4. 값이 가져와지고 그 값을 state에 저장한다. 

이와 관련해서 좀 찾아보니 공식문서에 이렇게 나와있더군요. 

 

 

useEffect는 componentDidMount()와 componentDidUpdate(), componentWillUnmount() 이렇게 세가지를 동시에 수행해주기 때문에 우리가 집중적으로 봐야하는 것은 HTML에 적은 우리의 상태값들을 마운트해주는 componentDidMount입니다. 

 

그런데 공식문서에서 어머 세상에 render()가 호출되고 componentDidMount()가 호출되네요? 

 

이거때문에 회사에서는 ref를 이용해서 값을 콱하고 박아주라고 하더군요. 

 

const [item, _setItem] = useState();
const itemInput = useRef();

const 콱박아주기 = (value) => {
	_setItem(value);
	itemInput.current.value(value);
}

 

뭐 대충 이런식으로 쓰라고 하던데 도저히 상식적인 행동이라고 보여지지 않아서 퇴근하고 집에와서 좀 찾아봤습니다. 

 

좀 찾아보니 이렇게 사용하면 되더군요. 

 

const [item, setItem] = useState();

useEffect(() => {
	const response = axios.get('/test');
    const data = response.data;
    setItem(data);
}, [])

return (
	<div>
    	{item && (
        	<input type="text" name="item" value={item}/>
        )}
    </div>
)

 

이렇게 하면 처음엔 input 태그가 등장하지 않지만 item의 상태가 바뀐 것을 리액트가 인지하고 해당 부분을 리렌더링 해주는 것입니다. 

 

뒤로 구르면서 봐도 맨처음 방법보단 훨씬 나은 것 같습니다. 

 

그런데 이렇게 하면 문제가 있더군요. 바로 useRef()를 사용할 수 없다는 것입니다. 

 

그 이유는 아래와 같은 순서로 리액트가 useRef()를 마운트해주기 때문입니다. 

 

  1. 렌더링 (이땐 input 태그가 렌더링 안됨). 이때 useRef() 마운트
  2. useEffect 호출
  3. API가져와서 setItem으로 상태변경
  4. 어? 상태 변경됐네? <input> 태그 리렌더링

 

이 상태에서 맨 처음 렌더링될 때 useRef()로 레퍼런스를 가져오는데 그에 상응하는 input태그가 없으니 undefined가 뜨고 이 ref에 접근해서 값을 바꿔주려고하면 에러가 뜹니다. 

 

 

저는 video태그를 이용해서 currentTime을 바꿔주려고 했던건데 current에 undefined가 뜨면서 세팅을 해줄 수 없었습니다. 

 

그래서 저는 꼼수를 부려서 이 태그를 컴포넌트로 따로 분리해서 컴포넌트에서 ref를 사용했습니다. 이렇게 사용하면 되긴 되더군요... 

 

import axios from 'axios'
import {useEffect, useRef, useState} from 'react'
import Video from './Video';
function App() {
  const [item, setItem] = useState('');

  const fetchData = async () => {
    const response = await axios.get('/item/test', {headers: {"Content-Type": "application/json"}});
    const data  = response.data;
    setItem(data);
  }
  useEffect(() => {
    fetchData();
  }, [])

  return (
    <div className="App">
      {item && (
        <Video startTime={30} fileName={item.fullVideo.screenVideo[0].fileName}/>
      )}
    </div>
  );
}

export default App;

 

import { useRef, useEffect } from "react";

function Video(props) {
    const video = useRef();
    const {startTime, fileName} = props;

    useEffect(() => {
        video.current.currentTime = startTime;
    }, [startTime])

    return (
        <video controls autoPlay ref={video} type='video/mp4'>
          <source src={'/record/'+fileName}></source>
        </video>
    )
}

export default Video;

 

이게 정석인지는 모르겠습니다... 그냥 제 머리속에서 떠오른거라 확실하진 않습니다. 

 

에러 없이 잘 되네요. 제가 원하는대로 30초로 이동도 잘 됐습니다. 

 

마치며

오늘은 그지같은 리액트를 한껏 저주하며 마무리지어보도록 하겠습니다. 프론트는 백이랑 다르게 센스가 좀 필요한 것 같습니다. 근데 저는 그게 부족한 것 같네요... 

 

그래도 실무에서 부딪히면서 해야만하는 환경에 저를 던져놓으니까 확실히 빠르게 실력이 늘어나는 기분이 들긴 합니다. 그지같은건 어쩔 수 없겠네요. 

 

긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~

 

 

출처

https://legacy.reactjs.org/docs/react-component.html#the-component-lifecycle

 

React.Component – React

A JavaScript library for building user interfaces

legacy.reactjs.org

 

'기타 > 리액트' 카테고리의 다른 글

리액트 전역 상태 관리 (redux, recoil)  (0) 2024.06.16
리액트 useReducer  (0) 2024.06.15