본문 바로가기
FE/react

[React] useState, useEffect, useRef

by s0ojin 2024. 1. 28.

 

 

 

 

 

본 글은 <모던 리액트 DEEP DIVE>도서의 내용을 바탕으로 작성되었습니다.

 

 

 

 

useState


useState는 함수형 컴포넌트 내부에서 상태를 정의하고, 이 상태를 관리할 수 있게 해주는 훅이다.

import { useState } from 'react'

const [state,setState] = useState(initialState)

 

  • 초기값을 넘겨주지 않으면 초기값은 undefined가 된다.
  • useState없이 변수를 이용해 상태값을 관리하려고하면 렌더링을 트리거시키지 못하거나, 강제 렌더링을 시킨다고 해도 값을 다시 초기화시켜 값을 변경시키지 못한다.
  • 그럼 useState훅의 결과 값은 어떻게 함수가 실행되어도 그 값을 유지하고 있는지에 대한 의문이든다. 아마 useState는 state값을 기억하기 위해  클로저로 구현되어있을 것으로 예상된다.
실제 리액트 레포에서 훅에 대한 구현체를 타고 올라가보면 일반 사용자가 접근하여 사용하거나 참고하는 것을 권장하지 않기 때문에 정확한 구현방식을 알 수 없지만 코드가 공개되어있는 Preact라는 리액트 경량화 버전의 코드를 바탕으로 유추해볼 수 있다.

 

 

게으른 초기화(lazy initialization)

  • 일반적으로 useState를 사용할 때 초기값으로 원시값을 주는 경우가 대부분이다. 그러나 원시값 말고도 함수를 인자로 넣어줄 수 있는데, 이것을 게으른 초기화라고 한다.
  • 이 게으른 초기화는 useState의 초기값이 복잡하거나 무거운 연산을 포함하고 있을 때 사용하라고 명시하고 있다. 게으른 초기화 함수는 state가 처음 만들어질 때만 실행되고, 이후 리렌더링 때는 무시된다.
  • 따라서 localStorage나 sessionStorage로의 접근, map/filter/find와 같은 배열에 대한 접근, 혹은 초기값 계산을 위해 함수호출이 필요할 때와 같이 무거운 연산을 포함해 실행비용이 많이 드는 경우에는 게으른 초기화를 사용하는 것이 좋다. 

 

useEffect


useEffect는 애플리케이션 내 컴포넌트의 여러 값들을 활용해 동기적으로 부수 효과를 만드는 메커니즘이다. 

function Component() {
    useEffect(()=>{
    	//do something
    },[props,state])
}

 

  • 첫 번째 인자에는 부수효과가 포함된 함수가, 두 번째 인자로는 의존성 배열을 전달한다.
  • 의존성 배열이 변경될 때마다 useEffect의 첫 번째 인수인 콜백을 실행한다.
  • useEffect는 어떻게 의존성 배열이 변경된 것을 알고 실행될까? 리액트는 모든 렌더링 시 마다의 고유의 state와 props를 갖고 있다. proxy, 옵저버, 데이터바인딩과 같은 특별한 기능을 통해 값의 변화를 관찰하는 것이 아니고, 렌더링할 때마다 의존성에 있는 값을 보면서 의존성 값이 이전과 다른게 있다면 부수효과를 실행하는 평범한 함수이다. 
  • useEffect은 의존성 배열의 이전 값과 현재값을 Object.is를 기반으로 얕은비교를 수행하고, 이전 배열과 현재 배열 값에 변경사항이 있다면 callback으로 선언한 부수효과를 실행한다.

 

클린업 함수의 목적

  • 클린업 함수는 일반적으로 이벤트를 등록하고 지울 때 사용해야 한다고 알려져 있다.
  • 클린업 함수는 새로운 값과 함께 렌더링된 뒤에 실행되지만 변경된 값을 읽는 것이아니라 함수가 정의 되었을 당시에 선언되었던 이전 값을 보고  실행된다.
  • useEffect는 그 콜백이 실행될 때마다 이전의 클린업 함수가 존재한다면 그 클린업 함수를 실행한 뒤에 콜백을 실행한다. 따라서 이벤트를 추가하기 전에 이전에 등록한 이벤트핸들러를 삭제해야  이벤트의 핸들러가 무한히 추가되는 것을 막을 수 있다.
  • 따라서 클린업 함수는 함수형 컴포넌트가 리렌더링 되었을 때 의존성 변화가 있었을 당시 이전의 값을 기준으로 실행되는, 말 그대로 클린업의 개념으로 보아야하는 것이지, 생명주기 메서드의 언마운트와는 다른 개념이다. 

 

의존성 배열

  • 빈 배열을 넘기면: 최초 렌더링 이후에 실행하지 않음
  • 아무 것도 넘기지 않으면: 렌더링 발생마다 실행
아무것도 넘기지 않을 때 렌더링 발생마다 실행된다면 useEffect없이 사용해도 되는 것이 아닐까?
// 1
function Component() {
  console.log('렌더링됨')
}

// 2
function Component() {
  useEffect(()=>{
    console.log('렌더링됨')
  })
}

 

1. 서버사이드 렌더링 관점에서 useEffect는 클라이언트 사이드에서 실행되는 것을 보장해 준다.
2. useEffect는 컴포넌트 렌더링의 부수효과, 즉 컴포넌트의 렌더링이 완료된 이후에 실행된다. 반면 직접실행은 컴포넌트가 렌더링되는 도중에 실행된다. 따라서 1번과는 달리 서버 사이드 렌더링의 경우에 서버에서도 실행된다. 그리고 이 작업은 곧 함수형 컴포넌트의 반환을 지연시키는 행위이다. 즉, 무거운 작업일 경우 렌더링을 방해하므로 성능에 악영향을 미칠 수 있다.

 

 

useEffect 사용 시 주의할 점

 

1. eslint-disable-line react-hook/exhaustive-deps 주석을 자제하라
: react/exhaustive-deps는 hook에서 state, props, 함수를 사용하고 있으면 의존성 배열에 넣어달라는 warning이다. 이 warning을 피하기위해 의존성 배열을 채우거나, 아예 넘기지 않는 등의 조치를 취하면 의도대로 작동하지 않을 때가 많다. 따라서 lint 무시를 위한 주석을 달곤하는데, 이는 권장되지 않는다. 애초에 빈 배열을 넘기는 행위는 마운트되었을 때 무언가를 하고싶다는 의도로 작성된 것으로, 생명주기 메서드인 componentDidMount에 기반한 접근인 것이다. 이럴 땐 최초의 함수형 컴포넌트가 마운트 됐을 때만 콜백함수 실행이 정말 필요한 것인가에 대해 다시 생각해봐야한다. 만약 정말 그렇다면, useEfffect 내 부수효과가 실행될 위치가 잘못되었을 가능성이 크다.

 

function Componenet({log}: {log: strgin}) {
  useEffect(()=>{
    logging(log)
  }, []) //exlint-desable-line react-hooks/exhaustive-deps
}

 

이 예제의 의도는 아마 log가 최초로 props로 넘어와 컴포넌트가 최초로 렌더링된 시점에만 logging을 실행하고 싶어서일 것이다. 그러나 log가 아무리 변하더라도 useEffect의 부수효과가 실행되지않으니, useEffect의 흐름과 컴포넌트의 props.log의 흐름이 맞지 않게된다.

따라서 logging이라는 작업은 부모 컴포넌트에서 실행되는 것이 옳을지도 모른다. useEffect에 빈 배열을 넘기기 전에는 꼭 정말로 useEffect의 부수효과가 컴포넌트의 상태와 별개로 작동해야만 하는지, 혹은 여기서 호출하는게 최선인지를 한번 더 검토해야한다.

 

2. useEffect의 첫 번째 인수에 함수명을 부여하라

: useEffect사용 시 많은 경우에 익명함수를 넘겨주는 예시가 많은데, 이는 리액트 공식문서에서도 마찬가지이다. 그러나 코드가 복잡해질 수록 useEffect가 무슨 일을 하는지 파악하기 어렵기때문에 적절한 이름을 가진 기명함수로 작성하면 좋다. 

useEffect(
  function logActiveUser() {
    logging(user.id)
  },
  [user.id]
)

 

3. 거대한 useEffect를 만들지 마라

: useEffect는 의존성배열이 변경될 때마다 실행되기때문에 부수효과의 크기가 커질수록 애플리케이션 성능에 악영향을 미친다. 따라서 가능하다면 useEffect는 간결하고 가볍게 유지하는 것이 좋다. 부득이하게 큰 useEffect를 만들어야한다면 적은 의존성 배열을 사용하는 여러 개의 useEffect로 분리하는 것이 더 낫다.

 

4. 불필요한 외부 함수를 만들지 마라

: useEffect내에서 사용할 부수 효과라면 내부에서 만들어서 정의해서 사용하는 편이 좋다.

왜 useEffect의 콜백 인수로 비동기 함수를 바로 넣을 수 없을까?
: 비동기함수의 속도에 따라 의도치않게 state의 값이 이전 기반으로 결과가 나올 수 있다. 이런 문제를 useEffect의 경쟁상태(race condition)이라고한다. 그러나 useEffect 내부에서 비동기 함수를 선언해 실행하거나, 즉시 실행 비동기 함수를 만들어서 사용하는 것은 가능하다.  정리하자면, 비동기 useEffect는 state의 경쟁상태를 야기할 수 있고, cleanup함수의 실행 순서도 보장할 수 없기 때문에 개발자 편의를 위하여 useEffect가 비동기함술를 인자로 받지 않는 것이다.

 

 

useRef


useRef는 useState와 동일하게 컴포넌트 내부에 렌더링이 일어나도 변경 가능한 상태값을 저장한다는 공통점이 있다. 그러나 useState와 크게 두가지가 다르다

  • useRef는 반환값인 객체 내부에 있는 current로 값에 접근 또는 변경할 수 있다.
  • useRef는 그 값이 변하더라도 렌더링을 발생시키지 않는다.
그냥 함수 밖에서 변수를 선언하여  관리하는 것과 뭐가 다를까? 이 때의 단점을 알아보자
1. 먼저 컴포넌트가 실행되기 전부터 value값이 기본적으로 존재하기 때문에 메모리에 불필요한 값을 갖게하는 악영향을 미친다.
2. 만약 컴포넌트가 여러 번 생성된다면 각 컴포넌트가 가리키는 값이 모두 동일하게된다. 컴포넌트 초기화 지점이 다르다고 해도 하나의 값을 봐야하는 경우라면 유효하겠지만, 대부분의 경우 컴포넌트 인스턴스 당 하나의 값을 필요로 하는 것이 일반적이다.

 

 

 

 

댓글