공부/React

리액트 숙련주차 - 3

뀨뿌뀨뿌 2023. 7. 5. 00:12

1. useContext(Context API)

ⅰ. useContext란?

  • react context 의 필요성
    • 부모컴포넌트 -> 자식 컴포넌트로 데이터를 전달해 줄 때 props를 사용함
    • 부모 -> 자식 -> 그 자식 -> 그자식의 자식 이렇게 너무 깊어지게 되면 prop drilling현상이 일어나게됨
    • porps drilling의 문제점은
      • 깊이가 너무 깊어지면 이 prop가 어떤 컴포넌트로부터 왔는지 파악이 어려워짐
      • 어떤 컴포넌트에서 오류가 발생할 경우 추적이 힘들어져 대처가 늦어짐
    • 그래서 등장한것이 react context API => useContext hook을 통해 쉽게 전역 데이터를 관리할 수 있게 됨

출처 : https://www.copycat.dev/blog/react-context/

ⅱ.  context API 필수 개념

  • createContext: context 생성
  • Consumer: context 변화 감지
  • Provider: context 전달(to 하위 컴포넌트)

 ⅲ. 코드 구현해보기

  • useContext를 사용하지 않았을때
// App.js
import React from "react";
import GrandFater from "./components/GrandFater";

const App = () => {
  return <GrandFater />;
};

export default App;

// components/GrandFater.jsx
import React from "react";
import Father from "./Father";

// GF => Child한테 어떤 정보를 알려줘서 Child가 그 내용을 출력하도록!
const GrandFater = () => {
  const houseName = "스파르타";
  const pocketMoney = 10000;
  return <Father houseName={houseName} pocketMoney={pocketMoney} />;
};

export default GrandFater;

// components/Fater.js
import React from "react";
import Child from "./Child";

const Father = ({ houseName, pocketMoney }) => {
  return <Child houseName={houseName} pocketMoney={pocketMoney} />;
};

export default Father;

// components/Child.js
import React from "react";

const style = {
  color: "red",
  fontWeights: "900",
};

const Child = ({ houseName, pocketMoney }) => {
  return (
    <div>
      나는 이 집안의 막내에요!
      <br />
      할아버지가 우리 집 이름은 <span style={style}>{houseName}</span>이라고
      하셨어요.
      <br />
      게다가 용돈도 <span style={style}>{pocketMoney}</span>원 만큼이나
      주셨어요!!
    </div>
  );
};

export default Child;

📚 GrandFater 컴포넌트는 Child 컴포넌트에게 houseName pocketMoney를 전달해주기 위해 Fater 컴포넌트를 거칠 수 밖에 없음.
=> 중간 컴포넌트가 많아지면 비효율적

  • useContext를 사용했을 때
// context/FamilyContext.js 
import { createContext } from "react";

export const FamilyContext = createContext(null)

// GrandFater.jsx 수정
import React from "react";
import Father from "./Father";
import { FamilyContext } from "../context/FamilyContext";

// GF => Child한테 어떤 정보를 알려줘서 Child가 그 내용을 출력하도록!
const GrandFater = () => {
  const houseName = "스파르타";
  const pocketMoney = 10000;

  return (
    <FamilyContext.Provider value={{
      houseName,
      pocketMoney
    }}>
      <Father />;
    </FamilyContext.Provider>
  );
};

export default GrandFater;

// Father.jsx
import React from "react";
import Child from "./Child";

const Father = () => {
  return <Child />;
};

export default Father;

// Child.jsx
import React, { useContext } from "react";
import { FamilyContext } from "../context/FamilyContext";

const style = {
  color: "red",
  fontWeights: "900",
};

const Child = () => {
  const data = useContext(FamilyContext)
  return (
    <div>
      나는 이 집안의 막내에요!
      <br />
      할아버지가 우리 집 이름은 <span style={style}>{data.houseName}</span>이라고
      하셨어요.
      <br />
      게다가 용돈도 <span style={style}>{data.pocketMoney}</span>원 만큼이나
      주셨어요!!
    </div>
  );
};

export default Child;

ⅳ. 주의 사항

  • 렌더링 문제
    • useContext를 사용할때 Provider에서 제공한 value가 달라진다면 useContext를 사용하고 있는 모든 컴포넌트가 리렌더링 됨
      => value 부분을 항상 신경써야함

2. 최적화 (React.memo, useCallback, useMemo)

ⅰ.  리-렌더링의 발생 조건과  최적화

  • 리-렌더링의 발생 조건
    • 컴포넌트에서 state가 바뀌었을 때
    • 컴포넌트가 내려받은 props가 변경되었을 때
    • 부모 컴포넌트가 리-렌더링 된 경우 자식 컴포넌트 모두
  • 최적화
    • 리액트에서 리렌더링이 번번하게 일어난다는것은 좋지 않음 => 비용이 발생하는 것은 최대한 줄여야함
    • 이러한 작업을 최적화(Optimization)이라고 부름
    • 리액트에서 불필요한 렌더링이 발생하지 않도록 최적화하는 대표적인 방법
      • memo(React.memo): 컴포넌트를 캐싱
      • useCallback: 함수를 캐싱
      • useMemo: 값을 캐싱

ⅱ.  memo(React.memo)

  • memo란?
    • 리-렌더링이 발생 조건 중 3번째 경우.
      => 부모 컴포넌트가 리렌더링 되면 자식 컴포넌트는 모두 리렌더링 됨
    • 자녀 컴포넌트 입장에서 바뀐게 없는데 다시 렌더링 되야하기 때문에 이부분을 돕는 도구가 바로 React.memo
  • 불필요한 리렌더링 예시
// App.js
import React, { useState } from "react";
import Box1 from "./components/Box1";
import Box2 from "./components/Box2";
import Box3 from "./components/Box3";

const App = () => {
  console.log("App 컴포넌트가 렌더링 되었습니다.");
  const [count, setCount] = useState(0);

  const onPlusButtonClickHandler = () => {
    setCount(count + 1);
  };

  const onMinusButtonClickHandler = () => {
    setCount(count - 1);
  };

  return (
    <>
      <h3>카운트 예제입니다!</h3>
      <p>현재 카운트 : {count}</p>
      <button onClick={onPlusButtonClickHandler}>+</button>
      <button onClick={onMinusButtonClickHandler}>-</button>
      <div style={{ display: "flex", marginTop: "10px" }}>
        <Box1 />
        <Box2 />
        <Box3 />
      </div>
    </>
  );
};

export default App;

// Box1.js
import React from "react";

const style = {
  width: "100px",
  height: "100px",
  backgroundColor: "#01c49f",
  color: "white",
};

const Box1 = () => {
  console.log("Box1 컴포넌트가 렌더링 되었습니다.");
  return <div style={style}>Box1</div>;
};

export default Box1;

// Box2.js
import React from 'react'

const style = {
  width: "100px",
  height: "100px",
  backgroundColor: "#4e93ed",
  color: "white",
};

const Box2 = () => {
  console.log("Box2 컴포넌트가 렌더링 되었습니다.")
  return (
    <div style={style}>Box2</div>
  )
}

export default Box2

// Box3.js
import React from 'react'

const style = {
  width: "100px",
  height: "100px",
  backgroundColor: "#c491be",
  color: "white",
};

const Box3 = () => {
  console.log("Box3 컴포넌트가 렌더링 되었습니다.")
  return (
    <div style={style}>box3</div>
  )
}

export default Box3

불필요한 리렌더링 발생과정 예시

  • memo를 통해 해결하기
    • React.memo를 이용해서 컴포넌트를 메모리(캐싱)에 저장해두고 필요할 때 가져다 쓸수 있음
    • 부모컴포넌트가 state의 변경으로 인해 props가 변경이 일어나지 않는 한 컴포넌트가 리렌더링 되지 않음
export default React.memo(Box1);
export default React.memo(Box2);
export default React.memo(Box3);

memo 사용

 ⅲ. useCallback

  • useCallack이란?
    • React.memo는 컴포넌트를 메모이제이션 했다면, useCallback은 인자로 들어오는 함수 자체를 기억(메모이제이션)함
  • useCallback의 필요성 예제
// React.memo예제에서 수정
// App.js
....
 // count를 초기화 하는 함수
  const initCount = () => {
    setCount(0)
  }

  return (
    <>
      <h3>카운트 예제입니다!</h3>
      <p>현재 카운트 : {count}</p>
      <button onClick={onPlusButtonClickHandler}>+</button>
      <button onClick={onMinusButtonClickHandler}>-</button>
      <div style={{ display: "flex", marginTop: "10px" }}>
        <Box1 initCount={initCount} />
 
 .....
 // Box1.js
 import React from "react";

const style = {
  width: "100px",
  height: "100px",
  backgroundColor: "#01c49f",
  color: "white",
};

const Box1 = ({ initCount }) => {
  console.log("Box1 컴포넌트가 렌더링 되었습니다.");
  return (
    <div style={style}>
      <button onClick={initCount}>초기화</button>
    </div>
  );
};

export default React.memo(Box1);

useCallback 필요성

📚 App컴포넌트와 Box1컴포넌트가 같이 렌더링 되는 이유
- Box1이 React.memo인 상태에도 불구하고 +, -버튼을 누르면 App컴포넌트와 Box1컴포넌트가 같이 렌더링됨
=> 함수형 컴포넌트를 사용했기 때문! - App.jsx는 컴포넌트지만 결국에는 함수이기 때문에
- count를 초기화 되는 함수도 App 컴포넌트가 리렌더링 될 때 다시 만들어짐 -> Box1에 있는 onClick 부분도 initCountrk 다시 만들어졌기 때문에 props로 새로운 값을 전달 받은거라고 인식함
- 실질적으로 initCount는 바뀐게 없음
=> React가 state가 바뀌었는지를 인지하기 위해서 객체나 함수 같은 것들은 불변성을 유지 해주는 방법을 이용해서 바꿔 줘야하는데 이 함수도 자바스크립트에서는 객체의 한 종류
저장될 때 별도의 공간을 바라보고 있는 주소값을 저정함 -> 함수형 컴포넌트가 리렌더링 되면서 안에 있는 함수들이 다시 만들어질 때 이전에 있었던 함수는 그대로 있고 새로운 값을 새로운 공간에 저장하고 그 주소를 다시 return 해줌

  • useCallback을 사용해서 함수 메모이제이션 하기
// App.jsx
// useCallback을 사용할땐 뒤에 의존성 배열을 같이 넣어주어야함
const initCount = useCallback(() => {
    setCount(0)
  }, [])

useCallback 적용하기

📚 useCallback이 특정 state가 변경될때, 저장했던 콜백함수가 갱신되어야 하면 의존성 배열부분에 해당 state를 넣어야만 함

  • useCallback 동작 원리

useCallback 원리

  • 더 나아가기
// App.jsx
  // count를 초기화 하는 함수
  const initCount = useCallback(() => {
    console.log(`${count}에서 0으로 변경되었습니다.`)
    setCount(0)
  }, [])

- count 가 4일 때 초기화 버튼을 눌렀는데 콘솔에서 0에서 0으로 변경으로 나타남
❓useCallback count 가 0일 때의 시점을 기준으로 메모리 함수에 저장했기 때문에
- 위에 문제를 해결하기 위해서는 dependency array 가 필요함

 // count를 초기화 하는 함수
  const initCount = useCallback(() => {
    console.log(`${count}에서 0으로 변경되었습니다.`)
    setCount(0)
  }, [count])

ⅳ. useMemo

  • useMemo란?
    • memos는 memoization을 뜻함 =>기억을 한다라는 말
    • 동일한 값을 반환하는 함수를 계속 호출해야 하면 필요없는 렌더링을 한다고 볼 수 있음
    • 맨 처음 해당 값을 반환할 때 그 값을 특별한 곳(메모리)에 저장함
      필요할 때 마다 다시 함수를 호출해서 계산하는 것이 아니라 이미 저장한 값을 단순히 꺼내와서 쓸 수 있음
    • 이러한 기법을 캐싱한다 라고 표현함
  • 문제 발생 상황
// App.jsx
import React from "react";
import HeavyComponent from "./components/HeavyComponent";

// heavy work 함수 => 무거운 작업
const App = () => {
  return (
    <>
      <nav style={{ backgroundColor: "yellow", marginBottom: "30px" }}>
        네비게이션 바
      </nav>
      <HeavyComponent />
      <footer style={{ backgroundColor: "green", marginBottom: "30px" }}>푸터 영역이에요</footer>
    </>
  );
};

export default App;

// components/HeavyComponent.jsx

import React, { useState } from "react";

const HeavyComponent = () => {
  const [count, setCount] = useState(0);

  const heavyWork = () => {
    for (let i = 0; i < 1000000000; i++) {}
    return 100;
  };

  const value = heavyWork();

  return (
    <>
      <p>나는 엄청 무거운 컴포넌트야!</p>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        누르면 아래 count가 올라가요!
      </button>
      <br />
      {count}
    </>
  );
};

export default HeavyComponent;

📍 heavyWork와 value는 컴포넌트가 리렌더링 되면 항상 호출되는 부분
heavyWork 함수는 항상 100을 return 하기 때문에 컴포넌트가 리렌더링 될때마다 수행한다는 것은 비효율적인 작업
버튼을 누르면 바로 업데이트가 되지 않고 약간의 딜레이가 발생 => 컴포넌트가 리렌더링 되면서 그분이 다시 호출됨

  • useMemo로 코드 개선시키기
// heavyComponent
  const value = useMemo(() => heavyWork(), []);
  console.log(`value는 ${value}`)
  • 또 다른 예시
import React, { useEffect, useState } from "react";

function ObjectComponent() {
  const [isAlive, setIsAlive] = useState(true);
  const [uselessCount, setUselessCount] = useState(0);

  const me = {
    name: "Ted Chang",
    age: 21,
    isAlive: isAlive ? "생존" : "사망",
  };

  useEffect(() => {
    console.log("생존여부가 바뀔 때만 호출해주세요!");
  }, [me]);

  return (
    <>
      <div>
        내 이름은 {me.name}이구, 나이는 {me.age}야!
      </div>
      <br />
      <div>
        <button
          onClick={() => {
            setIsAlive(!isAlive);
          }}
        >
          누르면 살았다가 죽었다가 해요
        </button>
        <br />
        생존여부 : {me.isAlive}
      </div>
      <hr />
      필요없는 숫자 영역이에요!
      <br />
      {uselessCount}
      <br />
      <button
        onClick={() => {
          setUselessCount(uselessCount + 1);
        }}
      >
        누르면 숫자가 올라가요
      </button>
    </>
  );
}

export default ObjectComponent;

❓useEffect를 이용해서 me의 정보가 바뀌었을 때만 발동되게끔 denpendency array를 넣어놨지만 count를 증가하는 button을 눌러보면 계속적으로 log가 찍히는 이유는?

- 불변성과 관련
- uselessCount state가 바뀌게 되면 리렌더링이 되고 컴포넌트 함수가 새로 호출된 후 me 객체도 다시 할당함(이때, 디른 메모리 주소값을 할당 받음)
- useEffect의 dependency array에 의해 me 객체가 바뀌었는지 확인해야하는데 이전 것과 동일한 값이지만 주소가 다르기 때문에 리액트에서는 me가 바뀌었다고 생각하고 useEffect 내부 로직이 호출됨

  • useMemo를 활용하여 해결하기
const me = useMemo(() => {
  return {
    name: "Ted Chang",
    age: 21,
    isAlive: isAlive ? "생존" : "사망",
  };
}, [isAlive]);

 

  • 주의해야 할 사항
    • useMemo를 남발하게 되면 별도의 메모리 확보를 너무 많이 하게 되기 때문에 오히려 성능이 학화될 수 있음
React.memo: 컴포넌트를 memoization
useCallback: 함수 자체를 기억해서 가져다 쓰는 함수의 memoization
useMemo: value에 대한 memoization => 함수가 return하는 값(값 자체)

 

'공부 > React' 카테고리의 다른 글

리액트 숙련주차 - 5  (0) 2023.07.11
리액트 숙련주차 - 4  (0) 2023.07.10
리액트 숙련주차 - 2  (0) 2023.06.30
리액트 숙련주차 - 1  (0) 2023.06.30
리액트 입문주차 1주차 - 5  (0) 2023.06.29