State 끌어올리기 & useEffect

1Hoit·2023년 2월 2일
0

React 기초

목록 보기
9/12

들어가기전 Check

  1. 리액트는 컴포넌트 기반으로 개발한다.
    또한 리액트 공식 매뉴얼에서는 함수 컴포넌트와 Hook을 사용하도록 권장하고 있다.

컴포넌트는 컴포넌트 바깥에서 props를 이용해 데이터를 마치 전달인자(arguments) 혹은 속성(attributes)처럼 전달받을 수 있다.
즉, 부모 컴포넌트에서 데이터를 props로 전달해 주어 자식 컴포넌트에서 해당 props를 받아서 사용할 수 있다.
이를 통해 리액트에서는 데이터가 위에서 아래로 전달되는 것을 알 수 있다.

결론 데이터의 흐름은 하향식이다.

  • 단방향 데이터 흐름이라는 원칙에 따라, 하위 컴포넌트는 상위 컴포넌트로부터 전달받은 데이터의 형태 혹은 타입이 무엇인지만 알 수 있습니다. 데이터가 state로부터 왔는지, 하드코딩으로 입력한 내용인지는 알지 못한다.
  1. Props & State에 대한 이해가 필요하다
    이전 포스트를 참고◁

props를 사용한다고 해서 값이 무조건 고정적이지는 않다.
부모 컴포넌트의 state를 자식 컴포넌트의 props로 전달하고, 자식 컴포넌트에서 특정 이벤트가 발생할 때 부모 컴포넌트의 메서드(상태변경 함수등..)를 호출하면 props도 유동적으로 사용할 수 있다.
이렇게 하려면 State 끌어올리기 가 필요하겠구나?

이게 살짝 헷갈리는데 뭔가 props는 외부로 부터 받고 읽기전용이라 변경되면 안된다고해서 은연중에 불변한다고 생각할 수 도있다. 하지만 내 생각엔 그런 개념이 아니라 클로저의 개념과 유사한것 같다.
클로저에서도 외부 함수의 변수를 내부 함수가 변경할 수 있었고 그 상태를 기억해주었기 때문에 이와 같이 생각했다. (나만의 생각일 뿐이다..이 부분은 좀 더 파봐야 겠다.)


Hook

  • Hook은 함수 컴포넌트에서 React state와 생명주기 기능(lifecycle features)을 “연동(hook into)“할 수 있게 해주는 함수이다.

  • Hook을 이용하여 기존 Class 바탕의 코드를 작성할 필요 없이 상태 값과 여러 React의 기능을 사용할 수 있게 되었다.
    (기존의 Class 바탕 코드는 추가적인 공부가 필요할 것 같다.)

Hook 특징

선택적으로 사용할 수 있으며 이전 리액트 이전 버전과의 호환성에 문제가 없다.
리액트 컨셉을 좀 더 직관적이게 접근할 수 있도록 API를 제공한다.

Hook은 왜 등장했을까?

  • Hook을 사용하면 컴포넌트로부터 상태 관련 로직을 추상화할 수 있다. 이를 이용해 독립적인 테스트와 재사용이 가능하다.
    Hook은 계층의 변화 없이 상태 관련 로직을 재사용할 수 있도록 도와준다.
    (아직은 이 말을 완벽하게 이해하진 못했다,,,,,,)

  • Hook을 통해 서로 비슷한 것을 하는 작은 함수의 묶음으로 컴포넌트를 나누는 방법을 사용할 수 있다. (구독 설정 및 데이터를 불러오는 것과 같은 로직) 또한 이러한 로직의 추적을 쉽게 할 수 있도록 리듀서를 활용해 컴포넌트의 지역 상태 값을 관리하도록 할 수 있다.

Hook 규칙

Hook은 JavaScript 함수이지만, 두 가지 규칙이 있다.

  1. 최상위(at the top level)에서만 Hook을 호출해야 한다.

    • 반복문, 조건문, 중첩된 함수 내에서 Hook을 실행하면 안된다.
    • 조건부로 effect를 실행하기 원한다면 Hook내부에 조건문을 넣어주면 된다.
  2. React 함수 컴포넌트 내에서만 Hook을 호출해야 한다.

    • 일반 JavaScript 함수에서는 Hook을 호출해서는 안 된다.
      (Hook을 호출할 수 있는 곳이 딱 한 군데 있는데 직접 작성한 custom Hook 내부이다.)--> 추후 알아보자

State 끌어올리기

도입부에서 props를 사용한다고 해서 값이 무조건 고정적이지는 않다. 라는 말과 함께 state 끌어올리기의 필요성을 언급하였다.

또한 부모컴포넌트의 상태가 하위 컴포넌트들에 의해 변한다면
State 끌어올리기가 필요하다.

개념 : 상위 컴포넌트의 "상태를 변경하는 함수" 그 자체를 하위 컴포넌트로 전달하고, 이 함수를 하위 컴포넌트가 실행한다

조금 구체화 해보자면 이렇다.

  • 부모컴포넌트에서는 useState를 통한 상태 관리를 해주며 상태변경 함수 (handler)를 하위 컴포넌트의 props로 전달하고
    하위 컴포넌트에서는 전달받은 props의 함수를 하위 컴포넌트의 이벤트 처리 함수에 넣어 실행하면 된다.

예를 들어보자

하위 컴포넌트(NewTweetForm)에서의 트윗 버튼클릭 이벤트가
부모(Twittler)의 상태를 바꾸어야만 하는 상황이다.
(부모는 자식에 의해 새로운 트윗 목록이 추가되면 전체 트윗 목록이 늘어나고 이를 다시 화면에 렌더링 해야 하는 상황)

상태를 변경시키는 함수(handler)를 하위 컴포넌트에 props로 전달해서 해결할 수 있다.

예제 코드

import React, { useState } from "react";
import "./styles.css";

const currentUser = "김코딩";

//부모 컴포넌트
function Twittler() {
  //부모 컴포넌트의 상태관리를 위한 useState 사용
  const [tweets, setTweets] = useState([
    {
      uuid: 1,
      writer: "김코딩",
      date: "2020-10-10",
      content: "안녕 리액트"
    },
    {
      uuid: 2,
      writer: "박해커",
      date: "2020-10-12",
      content: "좋아!"
    }
  ]);
  // 부모 컴포넌트 상태 변경 함수
  const addNewTweet = (newTweet) => {
    setTweets([...tweets, newTweet]);
  }; 

  return (
    <div>
      <div>작성자: {currentUser}</div>
      {/* 부모의 상태변경함수를 자식에게 props로 내려줌 */}
      <NewTweetForm onButtonClick={addNewTweet} />
      <ul id="tweets">
        {tweets.map((t) => (
          <SingleTweet key={t.uuid} writer={t.writer} date={t.date}>
            {t.content}
          </SingleTweet>
        ))}
      </ul>
    </div>
  );
}
//자식 컴포넌트는 부모로부터 props로 상태변경함수를 받아옴
function NewTweetForm({ onButtonClick }) {
  //자식의 상태관리
  const [newTweetContent, setNewTweetContent] = useState("");
  //자식의 상태 변경 함수
  const onTextChange = (e) => {
    setNewTweetContent(e.target.value);
  };
  // 자식의 클릭 이벤트를 위한 이벤트 핸들러
  const onClickSubmit = () => {
    let newTweet = {
      uuid: Math.floor(Math.random() * 10000),
      writer: currentUser,
      date: new Date().toISOString().substring(0, 10),
      content: newTweetContent
    };
    //부모의 상태 변경함수 실행
    //자식의 이벤트 발생시 변경된 상태를 부모에게 넘겨줌으로서
    //부모의 상태를 변경할 수 있게 된다.
    onButtonClick(newTweet);
  };

  return (
    <div id="writing-area">
    {/* onChange이벤트로 자식의 state가 변경된다.*/}
      <textarea id="new-tweet-content" onChange={onTextChange}></textarea>
    {/* onClick이벤트로 자식의 이벤트핸들러가 실행된다.*/}
      <button id="submit-new-tweet" onClick={onClickSubmit}>
        새 글 쓰기
      </button>
    </div>
  );
}

function SingleTweet({ writer, date, children }) {
  return (
    <li className="tweet">
      <div className="writer">{writer}</div>
      <div className="date">{date}</div>
      <div>{children}</div>
    </li>
  );
}

export default Twittler;

useEffect()

React에서 제공하는 useEffect()는 Side-Effect를 처리하기 위해 사용한다.

Side-Effect?

  • 함수가 실행되면서 함수 외부에 존재하는 값이나 상태를 변경시키는 등의 행위를 말한다.

예를들어 함수에서 전역변수의 값을 변경하거나 혹은 함수 외부에 존재하는 버튼의 텍스트를 변경하거나, 파일을 쓰거나, 쿠키 저장, 네트워크를 통해 데이터를 송신, React 컴포넌트 안에서 데이터를 가져오거나 구독하고, DOM을 직접 조작하는 작업 등이 있다.

Side-Effect를 우리가 따로 다뤄줘야 하는 이유

Side-Effect는 무조건 나쁜 패턴이라고 할 수 없다.

하지만 Side-Effect는 프로그램 가독성을 떨어뜨리고, 실행상태를 예측하기 어렵게 하며 개발비용을 증가시킨다고 보기 때문에 최근 선언형 프로그래밍에서는 Side-Effect를 최소화하려고 한다.

함수는 전달받은 매개변수를 통해 연산을 수행하고 결과를 반환해야 하며 그 결과는 항상 일관되고 예측할 수 있어야 프로그램이 쉽고 단순하며 유지보수 하기 쉬워지기 때문에 관리해야 한다.

순수함수

입력만이 함수의 결과에 영향을 주는 함수를 의미
그러므로 예측 가능한 함수이다. 늘 내가 생각했던 것과 같을 수 있다.

반대로 예측 불가능한 fetch API를 이용한 AJAX 요청이나 원본을 변경하는 메서드들은 순수함수가 아니다.

React에서 Side-Effect처리

React에서는 Side-Effect 처리를 위해 useEffect()함수를 제공한다.

함수가 매개변수를 받아 결과를 생성하는 것과 무관한 외부의 상태를 변경하거나 외부와 상호작용해야 하는 코드는 useEffect() 함수를 통해 분리해야한다.
왜? 결과가 일관되고 예측할 수 있어야 하기 때문이다.

코드로 보는 useEffect예시 1

나쁜예)

function UserProfile({ name }) {
  const message = `${name}님 환영합니다!`; //함수 반환 값 생성

  // Bad!
  document.title = `${name}의 개인정보`; //함수 외부와 상호작용하는 Side-effect 코드
  return <div>{message}</div>;
}

위의 코드는 함수형 컴포넌트가 실행되고 결과를 생성하는 것과 무관한 document.title을 수정한다.
이런 코드는 작은 프로그램을 개발할 때는 문제가 없겠지만, 다양한 개발자들이 대규모 프로그램을 협업 개발할 때 실행 상태를 예측하기 힘들게 한다. 만약 단순히 DOM을 수정하는 것이 아니라 무거운 작업을 수행하는 코드였다면 컴포넌트가 렌더링 될 때 마다 지연이 발생할 것이다. 이런 지연의 원인을 찾느라 시간을 쏟는다면 굉장히 비효율 적이다.

그래서 React에서는 이러한 Side-Effect 작업을 useEffect()를 통해서 분리할 수 있도록 지원하는 것이다.
useEffect()를 통해서 Side-Effect 코드를 등록할 수 있으며 React가 알아서 적절한 시점에 Side-Effect 작업을 수행한다.
이런 처리는 함수형 컴포넌트가 빠르게 렌더링 될 수 있게 도와주며, 프로그램을 복잡하게 만드는 Side-Effect 영역을 함수와 분리할 수 있게 도와준다.

위 코드를 수정한 좋은 예

function UserProfile({ name }) {
  const message = `${name}님 환영합니다!`;

  //Side-Effect 코드를 UseEffect로 분리
  useEffect(() => {
    document.title = `${name}의 개인정보`; 
  }, [name]);
  return <div>{message}</div>;
}

위와 같이 useEffect()를 사용하면 어떤 개발자라도 컴포넌트에 Side-Effect가 포함된 다는 것을 알 수 있고 React에서는 useEffect()에 등록된 Side-Effect 코드를 최적화된 시점에 실행하기 때문에 컴포넌트의 실행속도를 개선하는데도 도움이 될 것이다.

useEffect 내부에서 Side effect 관리하는 이유

useEffect 에 대해 알아보자

useEffect(함수);

구조 : 명령형 또는 어떤 effect를 발생하는 함수를 인자로 받는다.

동작시점

  • 기본적으로 동작은 모든 렌더링이 완료된 후에 수행되지만,
    특정 값이 변경되었을 때만 실행되게 할 수도 있다.
  • 기본적으로 첫번째 렌더링과 이후의 모든 업데이트에서 수행된다
    (렌더링이 완료된 후에 수행)
  • 리액트는 순수한 함수를 지향하기 때문에 render시 순수한 컴포넌트만을 render해주어야하는데 useEffect는 DOM객체가 이미 업데이트가 되었음을 보장하는 시점에 수행 된다.
  • useEffect는 side effect가 렌더링에 영향을 주지않도록 설계된 것이다.

여기서 잠깐 React 렌더링 과정을 살펴보자.


리액트의 공식문서를 보면 컴포넌트의 생명주기(컴포넌트의 생성, 갱신, 소멸)를 render phase 와 commit phase로 나누고 있다.

1. Render phase

우선 컴포넌트가 렌더되는 경우를 크게 두 가지로 나눌 수 있는데 첫번째는 initial render이고 두번째는 re-render이다.

  • 1-1. initial rendering 과정
    1. 우선 컴포넌트가 파싱되고 JSX가 React.createElement를 이용해서
      React elements로 컨버트된다.
      컨버트된 React elements는 메모리에 저장된다.
    2. 그 후 React elements를 이용해서 Virtual DOM이 생성되고
      commit phase에 보내진다.

여기서 React element는 메모리에서 DOM객체를 나타내는 자바스크립트 객체이다.

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

위의 컴포넌트를 babel transpiler에 넣어보면 이렇게 바뀐다.

const App = () = {
  return /*#_PURE_*/React.createElement("h1", null, "Hello World!");
}

즉 JSX파일은 React.createElement를 통해 React element로 메모리에 저장되고 이를 이용해 VDOM을 생성해 commit phase로 넘어가게 되는 것이다.

  • 1-2. re-rendering 과정
    리렌더링은 특정 컴포넌트의 상태가 업데이트 되거나 props가 바뀌면 실행된다.
    리렌더링 되는 컴포넌트에는 flag가 세워지고 해당 컴포넌트와 모든 자식 컴포넌트들이 리렌더링 된다.

    1. 상태가 바뀐 컴포넌트에 flag를 세운다.
    2. initial rendering과 마찬가지로 flag가 있는 컴포넌트와 자식
      컴포넌트들이 파싱되고 JSX가 React element로 바뀌면서 메모리에
      저장된다.
    3. 메모리에 저장된 React element로 Virtual DOM을 생성하고
      diffing 알고리즘을 이용해 이전 VDOM과 차이점을 비교한다.
    4. 차이점을 commit phase로 넘긴다.

2. Commit Phase

commit phase는 리액트가 실제 DOM을 조작하고 변화를 만들어내는 단계다.

여기서 변화를 만들어낸다는 것은 state나 props를 변경하고 sideEffect를 발생시킨다는 것이다.(api를 불러온다거나)

이 단계에서는 render phase 에서 diffing 알고리즘을 이용해서 발견한 이전 VDOM과 새로운 VDOM의(initial render에서는 이전 VDOM이 없지만) 차이점을 넘겨받게 된다.

그 후 발견된 변경점을 React DOM 라이브러리를 이용해 실제 DOM에 적용시킨다. DOM에 마운트 시키는것이지 화면에 paint하는 것은 아니다.

이제 useEffect안의 로직같은 sideEffect들이 실행되고 리액트는 다시 한 번 render phase와 commit phase를 거치게 된다.

위를 정리하자면
결국 외부에서 데이터를 받아오면(side effect) 컴포넌트가 생성될 때의
초기값을 바탕으로 Virtual DOM을 생성하고(render phase) 실제 DOM에
마운트 하는 과정(commit phase)를 거쳐 변경된 state를 바탕으로 다시
render와 commit phase를 거쳐 데이터를 갱신하는 과정이라는 것이다.

왜 Commit Phase에서 side effect 처리를 하는가?

가장 중요한 이유는 성능이 저하되기 때문이다.

가상돔의 가장 큰 장점은 리렌더링 할때 변경된 부분만 업데이트 할 수 있어서 성능에 큰 영향을 주지 않는다는 것이다.
이것이 리액트의 엄청난 장점 중 하나인데 만약 render phase에 side effect를 야기하는 과정을 포함시키면 side effect가 발생할 때마다 매번 Virtual DOM을 다시 그려야 한다. 또한 데이터 요청의 경우 동기적으로 네트워크 응답을 받아야만 다음 단계로 넘어갈 수 있다.

따라서 리액트는 컴포넌트 내부의 순수한 부분만을 사용해 화면을 그리고 난 후 side effect를 발생시킨다. 이후에 컴포넌트 갱신이 필요하여 다시 모든 과정을 거친다고 해도 Virtual DOM을 이용해 필요한 부분만 업데이트 하기 때문에 성능에 큰 영향이 없다.


다시 돌아가서 useEffect 구조를 살펴보자.

  1. 단 한번만 Effect 가 실행되려면?
    예) 외부 API를 통해 리소스를 받아오고 더 이상 API 호출이 필요하지 않을 때
useEffect(함수, [])

빈 배열을 useEffect의 두 번째 인자로 사용하면, 이때에는 컴포넌트가 처음 생성될 때만 effect 함수가 실행

  1. 특정 값이 변할 때만 실행 되려면?
useEffect(함수, [종속성1, 종속성2, ...])

배열 내의 어떤 값이 변할 때에만, (effect가 발생하는) 함수가 실행된다.

코드로 보는 useEffect 예시

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

위 코드를 기준으로 생각해보자

  • useEffect가 하는 일은?
    useEffect Hook을 이용하여 우리는 React에게 컴포넌트가 렌더링 이후에 어떤 일을 수행해야하는 지를 알려준다. React는 우리가 넘긴 함수를 기억했다가(이 함수를 ‘effect’라고 함) DOM 업데이트를 수행한 이후에 불러낼 것이다.

    effect를 통해 문서 타이틀을 지정하거나, 이 외에도 데이터를 가져오거나 다른 명령형(imperative) API를 불러내는 일도 할 수 있다.
  • useEffect를 컴포넌트 안에서 불러내는 이유?
    useEffect를 컴포넌트 내부에 둠으로써 effect를 통해 state 변수(또는 그 어떤 prop에도)에 접근할 수 있게 된다. 함수 범위 안에 존재하기 때문에 특별한 API 없이도 값을 얻을 수 있다.

    Hook은 자바스크립트의 클로저를 이용하여 React에 한정된 API를 고안하는 것보다 자바스크립트가 이미 가지고 있는 방법을 이용하여 문제를 해결한다.
  • useEffect는 렌더링 이후에 매번 수행되나?
    기본적으로 첫번째 렌더링과 이후의 모든 업데이트에서 수행된다
    effect는 렌더링 이후에 발생하는 것으로 생각하는 것이 더 쉽다. React는 effect가 수행되는 시점에 이미 DOM이 업데이트 되었음을 보장한다.

정리 (clean-up)를 이용하는 Effects

이밖에도 effect에 정리가 필요한 경우와 필요없는 경우가 있는데
위의 예제에서는 정리가 필요없어 어떤 것도 반환하지 않았다.
필요한 경우 추가하기!!

### 정리 (clean-up)를 이용하는 Effects 들도 있다.


Effeck Hook을 이용해 AJAX 요청

처음에는 왜? 라고 생각을 했다.

우리는 프론트엔드 단에서 원하는 데이터만 받아와 원하는 컴포넌트만 UI에 노출되게 하고 싶은 경우가 있다.
쉽게 말해 A를 검색했으면 A에 해당하는 내용만 클라이언트가 볼 수있게 하고 싶은 것이다.
즉, 클라이언트에서 서버로 요청하는 외부 API endpoint가 변경 되는 경우가 위와 같은 경우이다.

자 이제 종합해볼 시간이다.

우리는 useState를 통해 상태를 관리하는 방법을 배웠고
useEffect를 통해 Side Effects를 관리하는 방법을 배웠다.

만약 검색어에 따라 보여지고자 하는 Main.js의 컴포넌트가 변경된다면
즉, Search.js의 컴포넌트가 변함에 따라 Main에서도 영향을 받기 때문에 Side Effect가 발생하는 것이고 이 처리를 하려면 useEffet를 사용하여야 한다.

위와 같은 방식으로 동작을 하려면 Side Effect를 발생시키는 데이터 가져오기 fetch 요청을 useEffect 내부에서 처리해야한다.

AJAX 요청이 매우 느릴 경우?

모든 네트워크 요청이 항상 즉각적인 응답을 가져다주는 것은 아니다.
외부 API 접속이 느릴 경우를 고려하여, 로딩 화면(loading indicator)의 구현은 필수적이다.

Loading indicator의 구현

const [isLoading, setIsLoading] = useState(true);

// 생략, LoadingIndicator 컴포넌트는 별도로 구현했음을 가정
return {isLoading ? <LoadingIndicator /> : <div>로딩 완료 화면</div>}

fetch 요청의 전후로 setIsLoading을 설정해 주어 보다 나은 UX를 구현할 수 있다.

useEffect(() => {
  setIsLoading(true);
  fetch(`http://서버주소/proverbs?q=${filter}`)
    .then(resp => resp.json())
    .then(result => {
      setProverbs(result);
      setIsLoading(false);
    });
}, [filter]);

위 내용은 이곳을 참고하면 더 좋다◁

과제에 대한 내용은 첨부하는 것을 지양하라고 하였으므로 이 정도로 회고를 마치겠다.

출처 및 참고 : 코드스테이츠, ko.reactjs.org

profile
프론트엔드 개발자를 꿈꾸는 원호잇!

0개의 댓글