React 성능최적화 및 useEffect 훅

katanazero·2020년 8월 20일
1

react

목록 보기
7/15
post-thumbnail

useEffect 훅 사용시, 첫번째 인자는 부수효과함수 / 두번째 인자는 의존성배열을 인자로 갖는다고 하였다. 의존성 배열을 잘 관리하지 못하면 버그가 발생하고 디버깅이 어렵기 때문에 좀 더 이해를 해보자

useEffect(()=>{
  console.log('useEffect..');
}, []);
  • 의존성 배열은 useEffect 훅에 입력하는 두번째 인자다.
  • 의존성 배열의 내용이 변경됐을 때 부수효과함수가 실행된다.

useEffect 훅의 부수효과함수에서 API 를 호출하는 경우

export default User({userId}) {
  const [user, setUser] = useState(null);
  useEffect(()=>{
    getUser(userId).then(result => setUser(result.data.user));
  });

}

getUser() 함수를 호출하여 사용자를 조회해온다는 API 가 있다고 가정을 해보자.

User 컴포넌트가 렌더링을 할때마다 호출이 되면 비효율적이다. 이 문제를 해결하기 위해 의존성 배열에 빈 배열을 넣을 수도 있다. 하지만, userId 가 변경되도 새로운 사용자를 조회해오지 않는다면 올바른 해결책이 아니다.

export default User({userId}) {
  const [user, setUser] = useState(null);
  useEffect(()=>{
    getUser(userId).then(result => setUser(result.data.user));
  },[userId]);

}

userId 가 변경이 되었을때만, 부수효과함수를 실행하도록 해결한 코드다.
나중에 부수효과 함수를 수정할 때는 새로 추가된 변수를 빠짐없이 의존성 배열에 추가해야 한다.

export default User({userId, needDetail}) {
  const [user, setUser] = useState(null);

  useEffect(()=>{
    getUser(userId, needDetail).then(result => setUser(result.data.user));
  },[userId]);

}

needDetail 이라는 속성값을 부수효과 함수에서 사용했다. 새로운 상태값 또는 속성값을 사용했다면 의존성 배열에 추가해야한다. (사용자 조회 시, needDetail 이 바뀌었는데 조회를 해오지 않는다면 문제다)
그런데, 이게 값이 많아지면 깜빡하는경우도 발생한다.
-> 이를 해결하기 위해 eslint 에서 사용할 수 있는 exhaustive-deps 규칙을 사용하면, 잘못 사용된 의존성 배열을 찾아서 알려준다.

export default function MyComponent() {

  const [value1, setValue1] = useState(0);
  const [value2, setValue2] = useState(0);

  useEffect(()=>{
    const interval = setInterval(() => {
                       console.log(value1, value2);
                     },1000);
    return () => {
      clearInterval(interval);
    }

  },[value1]);

  return (
    <div>
      <button onClick={()=> setValue1(value1+1)}>value1 증가</button>
      <button onClick={()=> setValue2(value2+1)>value2 증가</button>
    </div>
  )

}

value2 는 의존성 배열에 넣지 않았다. value2 값을 증가시켜도 부수효과 함수는 갱신되지 않으며, value2 가 변경되기 전에 등록된 부수효과 함수가 계속 사용된다.
즉, 부수효과함수가 생성된 시점에 value2 를 참조하므로 value2 를 증가시켜도 초기값 0을 참조하는 것이다.

useEffect 훅에서 async ~ await 함수 사용하기

  • 부수효과함수를 async ~ await 함수로 만들면 에러 발생한다. 부수효과 함수의 반환값은 항상 함수타입이어야 한다.
const [user, setUser] = useState(null);
useEffect(async ()=>{
  const userData = await getUser();
  setUser(userData);
},[]);

promise 객체를 반환하므로 부수효과 함수가 될 수 없음.
useEffect 훅에서 async ~ await 함수를 사용하는 방법은 부수효과함수내에서 async ~ await 함수를 만들어서 호출하는거다.

const [user, setUser] = useState(null);
useEffect(()=>{
  async function getUserData() {
    const userData = await getUser();
    setUser(userData);
  }
  getUserData();
},[]);

😲 useEffect 훅에서 getUserData() 함수를 재사용해야한다면 어떻게 해야할까? 간단하다. useEffect 훅 밖으로 빼주면 된다.

const [user, setUser] = useState(null);
useEffect(()=>{
  getUserData();
},[getUserData]);

async function getUserData() {
    const userData = await getUser();
    setUser(userData);
}

useEffect 훅에서 getUserData() 함수를 사용하므로, 해당 함수를 의존성 배열에 추가해준다.
그런데 컴포넌트가 렌더링될때마다 getUserData() 함수는 갱신되므로 결과적으로 useEffect 훅은 렌더링될때마다 부수효과함수를 실행한다. 이 문제를 해결하려면 getUserData() 함수가 필요할 때만 갱신되도록 만들어야 한다. useCallback 훅을 이용해보자.

const [user, setUser] = useState(null);

useEffect(()=>{
  getUserData();
},[getUserData]);

const getUserData = useCallback(async ()=>{
  const userData = await getUser();
  setUser(userData);
},[]);

😌 useCallback 훅을 이용해서 불필요한 함수생성을 막았다.

의존성 배열을 없애는 방법

  • 의존성 배열을 사용하지 않는게 좋다. 의존성 배열을 관리하는데 많은 시간과 노력이 필요하기 때문이며, 함수를 의존성 배열에 넣는 순간 useCallback 훅 등을 사용해서 자주 변경되지 않도록 신경을 써야한다. 부수효과내에서 분기처리를 하여 실행 시점을 조절할 수 있다.
export default function User({userId}) {
  const [user, setUser] = useState(null);
  useEffect(()=>{
    
    if(!userId && !user && user.id !== userId) {
      getUser(userId)
   }    

  });
}

😌 조건문으로 getUser() 함수 호출시점을 관리한다.
의존성 배열을 입력하지 않으면, 부수효과함수에서 사용된 모든 변수는 가장 최신화된 값을 참조하므로 안심할 수 있다.
useState 훅의 상태값 변경함수에 함수로 인자를 전달하거나, useReducer 훅 사용, useRef 훅을 사용하여 의존성 배열을 사용하지 않게 개선이 가능하다.


리액트가 실행될 때, 가장 많은 자원을 사용하는 것은 렌더링이다. 리액트에서 최초 렌더링 이후에는 데이터 변경 시 렌더링을 하는데 다음과 같은 단계를 거친다.

  1. 이전 렌더링 결과를 재사용할지 판단
  2. 컴포넌트 함수를 호출
  3. 가상 DOM 끼리 비교해서 변경된 부분만 실제 DOM에 반영

React.memo() 함수로 렌더링 결과 재사용하기

  • 리액트는 컴포넌트의 상태값 및 속성값이 변경되면 해당 컴포넌트를 re-rendering 한다.
  • React.memo() 함수로 감싸서 생성한 컴포넌트라면 속성값 비교 함수가 호출되며, 이 함수는 이전 이후 속성값을 매개변수로 받아서 참 또는 거짓을 반환한다. 참을 반환하면 렌더링을 멈추고, 거짓을 반환하면 컴포넌트 함수를 실행해서 가상 DOM 을 업데이트한 후 변경된 부분만 실제 DOM 에 반영한다.
function User({name}) {

  return (
    <div>
     <p>{name}</p>
    </div>
  )
}

export default React.memo(User)

// 비교함수를 직접 작성도 가능하다.
export default React.memo(
  User,
  (prevProps, nextProps) => prevProps.name === nextProps.name
);

😌 이제 name 속성값이 변경된 경우에만 렌더링 된다.
React.memo() 함수의 두번째 인자인 속성값 비교함수를 입력하지 않으면 리액트에서 기본으로 제공하는 함수를 사용한다.

  • 기본 속성 비교함수는 속성값에 연결된 모든 속성을 비교한다.(얕은비교)
prevProps.prop1 === nextProps.prop1 && prevProps.prop2 === nextProps.prop2 && ...

속성값과 상태값을 불변 변수로 관리하기

  • 컴포넌트 내부에서 함수를 정의해서 자식 컴포넌트의 속성값으로 전달하면, 함수의 내용이 변경되지 않아도 자식컴포넌트 입장에서는 속성값이 변경됐다고 인식한다.
function Child({onClick}) {
  return (
   <div>
     <button onClick={onClick}>클릭 이벤트</button>
   </div>
  )  
}

export default Parent(){
  const [count, setCount] = useState(0);
  
  function clickEvent() {
    //...
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={()=>setCount(count+1)}>숫자값 증가</button>
      <Child onClick={()=>clickEvent}/>
    </div>
  )

}

Parent 컴포넌트의 속성값이 변경되면 렌더링이 발생을 하는데, 이때 clickEvent() 함수를 생성해서 자식 컴포넌트에게 전달을 해주는데 자식컴포넌트는 늘 속성값이 변경되었구나라고 인식한다.(실제로는 변경된 함수가 아님)
😈 React.memo() 함수로 Child 컴포넌트를 감싸서 생성하면 해결이 가능할까? 라는 생각도 들지만 onClick 속성값이 늘 새로운 함수로 들어가기때문에 소용이 없다.
useState, useReducer 훅의 상태값 변경 함수는 변하지 않는다는 점을 이용하면 이 문제를 쉽게 해결이 가능.
만약, 상태값 변경외에 다른 처리도 필요하다면 useCallback 훅을 사용할 수 있다.

function Child({onClick}) {
  return (
   <div>
     <button onClick={onClick}>클릭 이벤트</button>
   </div>
  )  
}

export default Parent(){
  const [count, setCount] = useState(0);
  
  const clickEvent = useCallback(() => {
    //...
  },[]);

  return (
    <div>
      <p>{count}</p>
      <button onClick={()=>setCount(count+1)}>숫자값 증가</button>
      <Child onClick={clickEvent}/>
    </div>
  )

}

useCallback 훅을 이용해서 이벤트 처리 함수를 구현했으며, 의존성 배열로 빈 배열을 입력했으므로 이 함수는 항상 고정된 값을 가짐.

객체의 값이 변하지 않도록 관리

  • 함수와 마찬가지로 컴포넌트 내부에서 객체를 정의해서 자식 컴포넌트 속성값으로 전다하면, 객체의 내용이 변경되지 않았는데도 속성값이 변경됐다고 인식한다.
function SelectItem({items, onChange}) {
 // ...
}


export default function SelectResult() {

  return (
    <div>
      <SelectItem items={[{name : 'item1', price : 100}, {name : 'item2', price : 200}]}
       onChange={//...}
      />
    </div>
  )

} 

SelectResult 컴포넌트가 렌더링될때마다 items 속성값에 새로운 참조값을 생성해서 전달한다.
items 속성값은 항상 같은 값을 가지므로 다음과 같이 컴포넌트 외부에서 상수로 관리할 수 있따.


function SelectItem({items, onChange}) {
 // ...
}


export default function SelectResult() {

  return (
    <div>
      <SelectItem items={ITEMS}
       onChange={//...}
      />
    </div>
  )

} 

const ITEMS = [{name : 'item1', price : 100}, {name : 'item2', price : 200}]

이제 items 속성값은 항상 같은값이 입력된다. 그런데 이번에는 컴포넌트 내부에서 계산되는 연산이 있는데, 이걸 최소환으로 실행하려면 useMemo 훅을 이용한다.

function SelectItem({items, onChange}) {
 // ...
}


export default function SelectResult() {

  const limitedPriceItems = useMemo(()=> ITEMS.filter(item => item.price <= 200), []);

  return (
    <div>
      <SelectItem items={ITEMS}
       onChange={//...}
      />
    </div>
  )

} 

const ITEMS = [{name : 'item1', price : 100}, {name : 'item2', price : 200}]

* 무조건 useMemo, useCallback, React.memo() 등을 사용하는 것은 바람직하지 않다. 성능 이슈가 발생했을 때 해당하는 부분의 코드만 최적화 하도록 하자!


가상 DOM 에서의 최적화

  • 요소의 타입을 변경하면 해당 요소의 모든 자식 요소도 같이 변경된다. 자식 요소의 내용이 변경되지 않아도 실제 DOM 에서 삭제되고 다시 추가되므로 비효율적이다.(리액트는 부모요소 타입이 변경되면 자식 요소 및 컴포넌트도 삭제 후에 다시 추가된다.)
  • 자식 컴포넌트도 삭제 후에 다시 추가가 되는거기 때문에 상태값이 초기화 된다.
export default App() {

   const [flag, setFlag] = useState(false);

   useEffect(() => {
    setTimeout(()=> {
       setFlag(true);
    }, 2000)
  }, [])

   return (
     if(flag) {
       return (
          <div> 
            <Child/>
            <p>메롱메롱<p/>
          </div>
       )
     } else {
          <span> 
            <Child/>
            <p>메롱메롱2<p/>
          </span>
     }
   )

}

2초 후에 div 요소로 변경된다. 이때, 부모요소가 변경되면서 자식컴포넌트 및 자식요소들이 삭제 후에 다시 추가된다.

  • 요소를 추가하거나, 삭제시 해당요소만 실제 DOM에 추가 또는 삭제하고 기존 요소는 건드리지 않는다.
export default App() {

   const [flag, setFlag] = useState(true);

   useEffect(() => {
    setTimeout(()=> {
       setFlag(false);
    }, 2000)
  }, [])

   return (
     if(flag) {
       return (
          <div> 
            <Child/>
            <p>메롱메롱1<p/>
          </div>
       )
     } else {
          <div> 
            <Child/>
            <p>메롱메롱1<p/>
            <p>메롱메롱2<p/>
          </div>
     }
   )

}

2초 후에 메롱메롱2 라는 요소를 추가한다. 가상 DOM 비교를 통해 앞의 두 요소가 변경되지 않았다는 것을 알며, 실제 DOM 에는 메롱메롱2만 추가한다.
그런데, 메롱메롱3 를 중간에 넣으면 어떻게 될까??

export default App() {

   const [flag, setFlag] = useState(true);

   useEffect(() => {
    setTimeout(()=> {
       setFlag(false);
    }, 2000)
  }, [])

   return (
     if(flag) {
       return (
          <div> 
            <Child/>
            <p>메롱메롱1<p/>
            <p>메롱메롱2<p/>
            <p>메롱메롱3<p/>
          </div>
       )
     } else {
          <div> 
            <Child/>
            <p>메롱메롱1<p/>
            <p>메롱메롱2<p/>
            <p>메롱메롱4<p/>
            <p>메롱메롱3<p/>
          </div>
     }
   )

}

중간에 요소를 추가하면 그 뒤에 있는 요소가 변경되지 않았다는 것을 알지 못함!!
메롱메롱3 가 변경되지 않았다는 것을 알기 위해서는 모든 값을 비교해야 하므로 연산량은 기하급수적으로 늘어난다. 리액트는 효율적으로 연산하기 위해 순서 정보를 이용한다.
이러한 문제는 key 속성값을 이용하면 해결이 가능하다. key 속성값을 입력하면 리액트는 같은 키를 가지는 요소끼리만 비교한다.

export default App() {

   const [flag, setFlag] = useState(true);

   useEffect(() => {
    setTimeout(()=> {
       setFlag(false);
    }, 2000)
  }, [])

   return (
     if(flag) {
       return (
          <div> 
            <Child key='child'/>
            <p key='메롱메롱1'>메롱메롱1<p/>
            <p key='메롱메롱2'>메롱메롱2<p/>
            <p key='메롱메롱3'>메롱메롱3<p/>
          </div>
       )
     } else {
          <div> 
            <Child key='child'/>
            <p key='메롱메롱1'>메롱메롱1<p/>
            <p key='메롱메롱2'>메롱메롱2<p/>
            <p key='메롱메롱4'>메롱메롱4<p/>
            <p key='메롱메롱3'>메롱메롱3<p/>
          </div>
     }
   )

}
  • key 속성값을 입력하면 같은 키를 가지는 요소끼리만 비교해서 변경점을 찾음
  • 메롱메롱4 key 는 새로 입력됐으므로 실제 DOM 에는 메롱메롱4 요소만 추가됨
  • key 속성값은 리액트가 렌더링을 효율적으로 할 수 있도록 우리가 제공하는 추가 정보. (대부분 ID 값을 입력하면 된다.)
  • 만약 key 속성값에 입력할 만한 값이 없다면 차선책으로 배열 index 정보를 입력이 가능하다. 하지만, 배열 중간에 원소를 추가하거나 삭제하는 경우 또는 순서를 변경하는 경우에는 비효율적으로 렌더링된다.

참고 : https://medium.com/sjk5766/react-%EB%B0%B0%EC%97%B4%EC%9D%98-index%EB%A5%BC-key%EB%A1%9C-%EC%93%B0%EB%A9%B4-%EC%95%88%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-3ce48b3a18fb)

profile
developer life started : 2016.06.07 ~ 흔한 Front-end 개발자

0개의 댓글