useState, useEffect

김zunyange·2023년 3월 2일
0
post-thumbnail

1. useState

const [state, setState] = useState(초기값);

useState hook의 호출 결과로 반환되는 배열의 요소는 각각 무엇일까?

  • 첫 번째 요소(state) 는 우리가 동적으로 관리하고자 하는 상태값입니다. 해당 상태의 초기값은 useState hook을 호출할 때 인자(defaultValue) 로 넘겨준다.
  • 두 번째 요소(setState function) 는 첫 번째 요소인 상태값을 업데이트 하는 함수다. state를 초기값에서 다른 값으로 변경하고 싶다면 setState를 통해서 변경해 주면 된다.
  • 첫 번째 요소와 두 번째 요소의 이름은 자유롭게 사용 가능

쉽게 말하자면,

  • useState는 함수형 컴포넌트에서 상태값을 관리하게 해준다.
  • initialState를 파라미터로 받고, state와 state를 변경할 setState함수를 반환한다.
  • 여기서 state 는 상태값이 저장되는 변수라고 생각하면 되고,
  • setState 는 이 state 상태값을 변경해주는 함수라고 생각하면 쉽다.
  • setState 함수가 호출돼서 state가 바뀌면 리액트는 이 컴포넌트를 다시 렌더링 해줍니다.

2-1. useState 예시 1

import { useState } from 'react';

const Example = () => {
  const [count, setCount] = useState(0);
  // "count"라는 새로운 상태 값을 정의합니다.
  return (
    <div>
      <p>{`count: ${count}`}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  )
};

export default Example;

🔎 useState가 반환하는 첫 번째 인자인 state와 두번째 인자인 setState를 비구조화 문법을 통해 count, setCount로 받아서 사용할 수 있게 된다.
setCount로 count state를 변경하면 렌더링이 다시 일어난다.

주의할 점은 클래스형 컴포넌트에서 setState를 하면 병합되지만, 함수형 컴포넌트에서는 이전 상태값을 지운다.

2-2. useState 예시 2

let [number, setNumber] = useState(0);
let [breath, setBreath] = useState(true);

const setIncrease = () => {
  setNumber(number + 1);
}

const setDecrease = () => {
  setNumber(number - 1);
}

return (
  <div>
  <button className="countUp" onClick={setIncrease} />
  <button className="countDown" onClick={setDecrease} />
   <p>{number}</p>
  </div>
)

🔎 countUp 버튼을 누르면 1씩 증가하고, countDown 버튼을 누르면 1씩 감소하게 되며, p 태그의 {number} 자리에는 초기값을 설정해둔 0이 처음에 보이게 된다.

🤓 useState는 여기까지 정리하고, 어느정도 이제 안 것 같다. 다행히 React에서 useEffect 보다 useState가 더 중요하다고 하지만, useEffect 가 더 어려운 것 같아 더 꼼꼼히 정리해보려 한다.


2. Side Effect

프로그래밍에서 부작용이란, 코드가 의도한 주된 효과 외에 추가적으로 발생하는 효과를 말한다. 특히 프로그램을 구성하는 가장 작은 단위인 '함수'에서 자주 사용되는 용어이다. 프로그래밍에서 함수가 하고자 하는 본질적인 역할, 주된 목적은 input을 받아서 output을 산출하는 것이다. 이를 식으로 표현하면 input ⇒ output으로, arrow function 의 형태이다. 따라서, 함수의 부작용(Side Effect)이란 함수의 목적인 input을 받아서 output을 산출하는 것 이외의 모든 행위를 의미합니다.


☝🏻 Side Effect 가 없는 ‘순수 함수’ ☝🏻

우리가 흔히 외부의 값이라고 하면 외부에 있는 변수만을 생각하기 쉽지만, DOM을 조작하고, console에 특정 문자를 출력하는 행위 또한 함수 외부에 존재하는 DOM과 console의 상태를 변경시키는 것이기에 아래 두 함수는 side effect가 발생한다고 할 수 있다.

정리하자면, 프로그래밍에서 side effect는 함수가 input을 받아서 output을 산출하는 과정에서

  1. 외부의 값을 읽어오는 행위
  2. 외부의 값을 변경하는 행위를 의미합니다.

그렇다면, 프로그래밍에서 side effect는 기피해야 하는 대상일까요? 맞다. 왜냐하면 side effect가 있는 함수는 동작 결과를 예측하기 쉽지 않기 때문입니다.

const sum = (x) => x + 1 이란 함수는 항상 우리가 1을 인자로 넣으면 2가 return 될 것이라 예측할 수 있지만, const sum = (x) => x + num 이란 함수는 num이란 값이 어떻게 변할지 모르기 때문에 함수의 결과를 예측하기 어려워진다. 따라서 side effect가 있는 함수들은 유지 보수할 때 개발자에게 어려움을 겪게 만들 수 있다.

하지만, 프로그래밍에서 외부의 값을 읽어오거나 변경하는 행위를 완전히 배제할 수는 없다. 프로그램이 외부로 그 어떤 값도 출력하지 않는다면 그 프로그램은 의미가 없고, 또한 데이터를 어딘가에 저장해두고 저장되어 있는 값을 읽어오는 행위는 프로그래밍에 있어서 필수불가결한 요소이기 때문이다.

따라서, 개발자들은 side effect를 최소화 하면서 프로그램을 설계하되, side effect가 필요한 경우에는 그것을 반드시 통제 가능하게 만들어서 side effect가 프로그램의 유지보수에 악영향을 주지 않도록 주의를 기울여야 한다.

2-1. React에서의 Side Effect

2-1-1. 함수 컴포넌트에서의 input과 output

React에서 rendering이란 state, props를 기반으로 UI 요소를 그려내는 행위이다.
우리는 React에서 화면을 컴포넌트 단위로 구성하고, 그 컴포넌트들은 React의 함수 컴포넌트를 이용해서 만들어낸다.

그렇다면, 함수 컴포넌트에서의 inputoutput은 무엇일까? 함수 컴포넌트는 state와 props를 통해서 JSX를 만들어내는 것이 본질적인 역할이다. 따라서 함수 컴포넌트의 input은 state와 props이며 output은 JSX라고 표현할 수 있다.

y = f(a,b)
UI = render (state,props)
(state, props) => JSX

🍀 정리하자면 React에서의 함수 컴포넌트는 state, props를 가지고 JSX를 만들어내는 함수이며, 이를 간단히 도식화하자면 (state, props) => JSX 라는식으로 함수 컴포넌트를 표현할 수 있다.

(state는 함수 컴포넌트에 인자로 전달되는 값이 아니라, 컴포넌트 내부에서 useState hook을 통해서 가져오는 것이지만 개념상으로는 외부에서 가져오는 값이기에 위와 같은 도식으로 표현함)

2-1-2. 함수 컴포넌트에서의 Side Effect

위에서 함수 컴포넌트는 state와 props라는 input을 통해서 JSX를 만들어내는 함수라고 이해했는데, 함수 컴포넌트에서의 side effect는 대표적으로 어떤 것이 있을까? 우리가 UI를 만들어내는 과정에서 발생시켜야 하는 side effect는 무궁무진하게 많겠지만 대표적인 사례를 몇 가지 알아보자.

Data Fetching

현대 개발에서 프론트엔드는 복잡한 UI를 구성하고 변화시키는 데에 초점을 두고 있고, 백엔드는 데이터를 저장하고 처리하고 가공하는 역할을 맡고 있습니다. 이러한 구조 속에서 프론트엔드가 백엔드 API를 통해서 기존에 저장된 데이터를 가져오는 행위는 필수적으로 발생하게 됩니다.

DOM 접근 및 조작

프론트엔드 개발자로서 웹 개발을 하는 이상 DOM에 접근하고 조작하는 행위는 필수적입니다. 물론, React는 DOM의 조작을 React에서 대신해주고 개발자는 UI와 핵심 로직에만 신경 쓰도록 선언적인 개발을 가능하게 해주기 때문에 DOM에 직접 접근할 일이 많지도 않고 대부분의 상황에서 권장되지 않지만 특정 상황(document 객체에 scroll eventListener를 등록하는 등)에서는 DOM에 접근하고 직접 조작을 해야 하는 상황이 발생합니다.

구독(Subscribe)

프로그래밍에서의 구독이란 어떤 것의 변화를 계속해서 지켜보고 변화가 발생하면 특정한 액션을 취하는 것 을 말합니다. 실생활의 예시를 보자면 유튜브에서 구독이란 단어를 많이 들어보셨을 겁니다. 유튜브에서 우리는 구독을 함으로써 해당 유튜버의 변화를 계속해서 지켜보고 만약 새로운 영상이 올라오면 알림을 받거나, 메인 피드에 노출시키는 액션을 발생시킵니다. 개발에서는 구독이란 개념을 이용해서 여러 가지 동작을 처리할 수 있지만, 우리가 웹 개발에서 흔히 구독하는 것은 시간입니다. 시간을 구독한다는 것이 와닿지 않을 수 있습니다. 시간은 고정돼있지 않고 항상 변화합니다. 당장 이 글을 읽는 동안도 계속해서 시간이 흐르고 있습니다. 그래서 자바스크립트는 시간을 구독하면서 일정 시간이 지나면 특정 동작을 수행해 주는 메서드인 setTimeout, 일정 시간마다 특정 동작을 수행해주는 setInterval 메서드를 제공해주고 이를 이용해서 시간의 변화에 따라 원하는 동작을 이행시킬 수 있습니다.

3. useEffect

useEffect()는 Side-Effect를 처리하기 위해 사용한다고 한다.
useEffect 이라는 컴포넌트가 마운트 됐을 때 (처음 나타났을 때), 언마운트 됐을 때 (사라질 때), 그리고 업데이트 될 때 (특정 props가 바뀔 때) 특정 작업을 처리해주는 hook 이다.

그렇다면 리액트에서 side effect는 언제, 어떻게 발생할까?

const App = () => {
  return <h1>Hello World</h1>;
};

리액트의 함수 컴포넌트는 이처럼 JSX를 리턴하고 그것이 UI에 표시되는 형태이기에 우리가 사이드 이펙트를 발생시키고자 했을 때 가장 일반적으로 하는 생각은 함수의 return문 위에서 바로 하고 싶은 동작(Side Effect) 실행시키는 것입이다. 그래서 아래와 같은 코드를 작성하게 된다.

const App = () => {
  const doSideEffect = () => {
    // 특정한 사이드 이펙트를 발생
  fetch(); // fetch 에서 막히게 되면 ui가 보이지 않게 됨
  };

  doSideEffect();
	  // 함수 호출

  return <h1>Hello World</h1>;
};

:렌더링될때 fetch 함수 요청

🔎 return 문 줄을 보면 거기서 JSX를 리턴하고 있고 리턴하기 전에 특정한 사이드 이펙트를 발생시키는 함수를 정의하고 그 함수를 호출하고 있다.

하지만 위와 같이 렌더링 단계(UI를 만들어내는 과정)에서 side effect를 발생시키게 되면 두 가지 문제가 발생한다.

  1. side effect가 렌더링을 blocking
  2. 매 렌더링마다 side effect가 수행

3-1. side effect가 렌더링을 blocking

const App = () => {
  const doSideEffect = () => {
    // 시간이 오래 걸리는 Side Effect
  };

  doSideEffect();
	  // 함수 호출

  return <h1>Hello World</h1>;
};

위 코드는 side-effect를 함수 컴포넌트 본문 안에서 실행시킨다.

기본적으로 코드는 위에서 아래 방향으로 순차적으로 실행된다. 따라서 App 함수 컴포넌트는doSideEffect() 동작이 끝날 때까지 JSX를 리턴하는 코드로 넘어가지 않는다. 컴포넌트가 JSX를 return 하기 전까지는 UI가 브라우저상에 렌더링 되지 않기 때문에 결국 사이드 이펙트가 끝나기 전까지 렌더링을 하지 못하고 멈춰있게 된다.

즉, 사용자가 UI가 업데이트되는 것을 보기까지 오랜 시간이 소요된다는 것이다. 이는 곧 사용자에게 좋지 못한 사용자 경험을 제공한다는 의미이다.

3-2. 매 렌더링마다 side effect가 수행

버튼을 누를때마다 alert창이 뜨는 등의 특정한 side effect들은 매번 실행될 필요가 없을 수도 있다. 예를 들어 인스타그램의 피드를 보여주는 UI를 생각해보면, 피드를 보여주기 위해서는 실제 이 피드에 대한 정보를 외부에서 가져와야 할 것이다. 즉, 피드 리스트를 보여주기 위해서는 최초에 피드 데이터들을 가져오는(Data Fetching) side effect가 필요하다. 이를 코드로 표현하자면 이런 형태의 코드가 될 것이다.

const App = () => {
  // 코드 생략

  // data fetching side effect
  getFeeds();

  return 피드리스트;
};

그런데 여기서 좋아요 기능이 필요해서 하트를 누를 때마다 생깔을 변경하는 UI의 변화가 필요하다면, 하트를 누를 때마다 컴포넌트에서 리렌더링을 발생시켜야 한다. 리액트에서 함수 컴포넌트가 리렌더링 된다는 것은 곧 함수 컴포넌트를 다시 한번 호출한다는 뜻이다. (리액트는 컴포넌트의 state나 props가 변하면 자동으로 해당 함수 컴포넌트를 다시 호출하면서 리렌더링을 수행해 줍니다).

즉, App이라는 함수가 다시 호출되고 그렇다면 다시 함수 내부의 코드를 위에서 아래 방향으로 순차적으로 실행시킨다. 그 말은 다시 한번 getFeeds()side effect를 발생시킨다는 의미이다.

이러한 데이터 패칭 등 특정한 상황에서 필요한 side effect가 매 렌더링마다 무조건 발생하게 된다면 이는 비효율적인 프로그램을 만들게 되는 것이다.

3-3. 실습1


☝🏻 useEffect 안에 side effect를 넣으면, 일단 멈추고 렌더링된 다음 useEffect 안에 있는 side effect가 실행된다. 아래 순서대로 내려갔다 중간에 있는 useEffect 함수로 다시 옴


☝🏻 2 → 1 → 2 → 1 → 2 → 1 ...
렌더링이 될때마다 sideEffect가 실행되기 때문에 클릭될때마다 2 → 1이 반복해서 찍힘 → 빈배열을 넣어서 반복안되게 하자 그게 뭐냣?!


☝🏻 2 → 1
의존성 배열 [] 안에 데이터가 없으면 최초 렌더링 됐을 때만 실행된다. 배열안에 아무 데이터가 없어도 한번은 실행되지만, 그 후에는 안 될 뿐!

3-3. 실습2


☝🏻 3 → 1 → 2

클릭버튼을 누르면 3 하나 (의존성배열인 빈 배열은 최초 마운트될때만 실행되기 때문)
state가 변하면 (=클릭하면) 리렌더링이 된다.
그리고 input에 text(name) 쓰면 3→2 (시점이 중요 !!)


☝🏻 name input 에 입력해도 3 → 2
age input에 입력해도 3 → 2
즉, 동시에 출력도 가능하다

3-4. 정리

위 내용을 통해서 우리가 React에서 side effect를 언제 발생시켜야 하는지 생각해 보면 side effect는

  1. 렌더링을 Blocking 하지 않기 위해서 렌더링이 모두 다 완료되고 난 후 실행할 수 있어야 한다.
  2. 매 렌더링마다 실행되는 것이 아니라 내가 원할 때만 조건부로 실행할 수 있어야 한다.

위 두 가지 조건을 충족시키면서 발생시키는 것이 가장 좋습니다. 다행히 React에서는 위의 요구사항을 모두 충족시키면서 편하게 side effect를 발생시킬 수 있게 도와주는 useEffect라는 훅(hook)이 이미 존재합니다.


👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻👇🏻

useEffect( () => {
	//작업
} );

🍀 컴퍼넌트가 렌더링될 때마다 콜백이 매번 실행
= 컴퍼넌트가 맨처음 화면에 렌더링될 때 그리고 컴퍼넌트가 다시 렌더링될 때 실행

useEffect( () => {
  //작업
}, [value] ); 

//여기서 배열을 dependecy array라고도 함

🍀 화면에 첫 렌더링 될때 실행 & value 값이 바뀔때 실행

  • 매번 렌더링이될 때마다 실행되는게 아니라 컴퍼넌트가 맨처음 화면에 렌더링될 때 그리고 배열 안에 들어있는 요소의 값이 바뀔때만 실행
  • 만약 빈 배열을 전달해준다면([value]가 아니라 []), 컴퍼넌트가 맨 처음 화면에 렌더링될 때만 실행

useEffect에 의존성 배열을 인자로 전달하게 되면 useEffect의 동작은 먼저 처음으로 의존성 배열의 값을 검사할 것이다. 검사한다라는 것은 의존성 배열에 들어가 있는 값이 이전 렌더링과 비교했을 때 동일한가 검사한다는 의미이다. 검사가 됐으면 이제 이 조건에 따라서 콜백함수를 호출해준다.

  • 첫번째 렌더링에서는 무조건 콜백함수를 호출
  • 두번째 렌더링부터는 의존성 배열의 값이 변했으면 콜백함수를 호출, 만약 변하지 않았다면 콜백함수를 호출하지 않음

useEffect 공식문서
useEffect API 참고서
chatGPT에게 물어보았다.


출처 : wecode

profile
배움은 즐거워 ~(*ૂ❛ᴗ❛*ૂ)

0개의 댓글