사이드 이펙트, useEffect

김세현·2022년 7월 14일
0

React

목록 보기
7/10

부수 효과, 사이드 이펙트

함수 내의 어떤 구현이 함수 외부에 영향을 끼치는 경우 해당 함수는 Side Effect가 있다고 말한다.

예를 들면 아래의 코드에서 bar는 외부의 전역 변수 foo에 영향을 미치기 때문에, Side Effect가 있다고 말할 수 있다.

let foo = 'hello';

function bar(){
  foo = 'world';
}

bar(); // bar는 Side Effect를 발생시킨다.

순수 함수

순수 함수란, 오직 함수의 입력만이 함수의 결과에 영향을 주는 함수를 말한다.

함수의 입력이 아닌 다른 값이 함수의 결과에 영향을 미치는 경우, 순수 함수라고 할 수 없다.

또한 순수 함수는 입력으로 전달된 값을 수정하지 않는다

예를 들어 아래의 코드에서 upper 함수는 순수 함수라고 할 수 있다.

function upper(str) {
  return str.toUpperCase(); 
  // toUpperCase 메소드는 원본을 수정하지 않는다. (Immutable)
}

upper('hello') // 'HELLO'

따라서 순수 함수에는 네트워크 요청과 같은 Side Effect가 없다.

순수 함수의 특징 중 하나는, 어떤 인자가 주어질 경우, 항상 똑같은 값이 반환되는 것을 보장한다.

따라서 예측 가능한 함수이기도 하다.

리액트의 함수 컴포넌트

React의 함수 컴포넌트는, props가 입력으로, JSX Element가 출력으로 나간다.

여기에는 그 어떤 Side Effect도 없으며, 순수 함수로 작동한다.

function User({ name,age }) {
  return (
    <div>
      <div>{name}</div>
      <div>{age}</div>  
    </div>
  )
}

하지만 보통 React 애플리케이션을 작성할 때에는, AJAX 요청이 필요하거나,

LocalStorage에 데이터를 저장하는 것 또는 타이머와 같은 React와 상관없는 API를 사용하는 경우가 발생할 수 있다.

이는 React의 입장에서는 전부 Side Effect 이다.

즉, Side Effect는 리액트가 화면을 그리기 위한 작업을 수행하는 것과 직접적으로 관련이 없는 작업이지만, 리액트 앱에서 충분히 발생할 수 있는 작업이다.

(물론, http 요청(=Side Effect)에 대한 응답으로 상태가 변경되어 화면의 UI가 다시 그려질 순 있지만,
이는 간접적인 역할이지 http 요청이 화면에 UI를 그리는 것에 직접적으로 관여하는 것은 아니다.)

따라서 리액트는 이러한 Side Effect도 관리해야 하기 때문에 Side Effect를 다루기 위한 Hook인 useEffect를 제공한다.


useEffect로 Side Effect 다루기 (1) : Web Storage API를 사용하여 로그인 여부 저장하기


간단한 로그인 및 로그아웃을 구현한 리액트 앱이다.

유효한 아이디 / 패스워드 정보를 입력하게 되면 로그인 상태에 따라 헤더가 변경되고 로그아웃 버튼을 누르면 로그아웃되는 기능을 가지고 있다.

하지만, 로그아웃 버튼을 눌렀을때 뿐만 아니라 페이지를 새로고침 했을 때에도 사용자의 로그인 정보가 사라지게 된다.

따라서 Web Storage APIlocalStorage 객체를 이용해 브라우저 저장소인 로컬

스토리지에 사용자 로그인 정보를 저장하고 애플리케이션이 시작할 때 이 정보를 읽어

사용자 로그인 여부에 따라 적절한 화면을 보여줄 수 있도록 해야 한다.

이 작업은 리액트가 화면을 그리는 작업과 관련이 없는 외부 API를 이용하는 작업이므로 Side Effect이다.

따라서 useEffect를 이용해 작업을 분리해야 한다.

먼저, useEffect를 사용하지 않는 경우를 살펴본다면,


function App() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  //useEffect를 사용하지 않고 App 컴포넌트 안에 코드를 그대로 넣었다.
  const storedUserLoggedInForm = localStorage.getItem("isLoggedIn");
  if (storedUserLoggedInForm === "1") {
    setIsLoggedIn(true);
  }

  const loginHandler = (email, password) => {
    localStorage.setItem("isLoggedIn", "1");
    setIsLoggedIn(true);
  };
  
  ...

사용자가 처음으로 로그인을 하게 되면 loginHandler 함수에 의해 브라우저의 저장소인 로컬 스토리지에 "isLoggedIn"라는 키와 "1"이라는 값이 등록된다.

로그인 이후에는 로컬 스토리지에 저장된 값을 읽고, 사용자 로그인 여부를 저장하는 상태값을 true로 설정하고 조건부 렌더링을 이용해 적절한 화면을 보여준다.

하지만, 이 코드는 사용자가 로그인한다면 무한 루프에 빠지게 되면서 에러가 발생하게 된다.

이유는 사용자가 처음으로 로그인 한다면, 로컬 스토리지의 값을 "1"로 저장하고하면서 상태 변경 함수를 호출하게 되고,

상태 변경 함수를 호출하게 되면, 상태가 변경되고 컴포넌트가 재렌더링 되면서 코드를 다시 실행하게 된다.

즉, 이미 로그인한 경우라면, 로컬 스토리지의 값은 항상 "1"이므로 아래의 코드에서

조건에 일치하게 되고, 상태 변경 함수 setIsLoggedIn를 다시 호출하게 된다.

리액트는 상태 변경 함수를 호출하게 되면, 컴포넌트를 재렌더링 하므로 App 컴포넌트의

코드를 다시 실행하면서 아래의 코드를 읽고, 다시 상태 변경 함수setIsLoggedIn

실행하면 컴포넌트가 재렌더링 되고 다시 코드를 읽고, 다시 로컬 스토리지의 값을 읽고, 다시 상태 변경 함수 setIsLoggedIn를 실행하고 ...

  const storedUserLoggedInForm = localStorage.getItem("isLoggedIn");
  if (storedUserLoggedInForm === "1") {
    setIsLoggedIn(true);
  }

무한 루프에 빠지게 된다.

사용자 로그인 정보를 읽어오는 작업은 앱이 처음 실행될 때 한 번만 수행하면 되므로

useEffect훅을 사용해 컴포넌트 코드와 분리하여 코드를 실행할 필요가 있다.

기본적으로 useEffect는 컴포넌트가 렌더링 되고나서 실행된다.

useEffect 훅은 두 매개변수를 받는다.

첫 번째 매개변수는 함수이며, 보통 익명 함수를 사용하여 Side Effect 코드를 여기에 작성한다.

두 번째 매개변수는 의존성 배열이다.

만약, useEffect를 사용한다 하더라도 두 번째 매개 변수의 값을 지정하지 않는다면, 위에서 작성한 코드와 똑같이 동작한다.

따라서 두 번째 매개변수를 명시하여 해당 의존성에 따라 useEffect 함수가 실행되게 해야 한다.

만약, 의존성 배열이 빈 배열이라면 해당 컴포넌트가 처음 렌더링된 이후에 한 번만 실행되며

의존성 배열안에 값들이 명시되어 있다면, 해당 값이 변경될 경우에만 useEffect 함수는 실행된다. (컴포넌트가 렌더링된 이후)

useEffect를 사용한 경우

  useEffect(() => {
    const storedUserLoggedInForm = localStorage.getItem("isLoggedIn");
    if (storedUserLoggedInForm === "1") {
      setIsLoggedIn(true);
    }
  }, []);

기존의 코드를 useEffect 함수 안의 첫 번째 매개 변수인 익명 함수의 코드로 넣어 주었으며, 두 번째 매개 변수에는 빈 배열을 넣어 주었다.

이렇게 작성하게 된다면, 컴포넌트가 처음으로 렌더링된 이후 로컬 스토리지에서

사용자 로그인 정보를 읽고, 상태 변경 함수를 호출하는 코드는 한 번만 실행된다.

따라서, 이전과 같은 무한 루프에 빠지게 되는 문제가 발생하지 않는다.

결론
1.리액트가 화면을 그리기 위해 수행하는 작업과 Side Effect의 작업을 useEffect를 이용해 분리해야 한다.
2.어떤 액션에 대한 응답으로 수행해야 할 코드가 있다면, useEffect를 이용하자.

profile
under the hood

0개의 댓글