리액트 숙련 - 리렌더링, 최적화, React Hooks(React.Memo, useCallback, useMemo), 라이프사이클, Dom과 가상Dom, Redux

새벽로즈·2023년 11월 8일
1

TIL

목록 보기
41/72
post-thumbnail

리렌더링의 발생 조건

  1. 컴포넌트에서 state가 바뀌었을 때
  2. 컴포넌트가 내려받은 props가 변경되었을 때
  3. 부모 컴포넌트가 리-렌더링 된 경우 자식 컴포넌트는 모두

최적화

  • 리렌더링이 자주 발생하는 것은 비용이 많이 소모되는데, 이를 최소화하기 위해 최적화가 필요함

  • 최적화 방법:
    1) memo(React.memo): 컴포넌트를 캐싱하여 불필요한 렌더링을 방지한다.
    2) useCallback: 함수를 캐싱하여 렌더링 시에 새로운 함수를 생성하는 비용을 줄인다.
    3) useMemo: 값을 캐싱하여 렌더링 시에 새로운 값이 계산되는 비용을 최소화한다.

memo (React.memo)

  • 리렌더링의 발생 조건 중 부모 컴포넌트가 리렌더링될 때 자식 컴포넌트도 모두 리렌더링되는 상황이 생김

  • React.memo는 자식 컴포넌트의 불필요한 리렌더링을 방지하기 위해 React에서 제공하는 메소드로, 컴포넌트의 리렌더링을 최적화하는데 사용됨

  • 예시로 부모 컴포넌트가 리렌더링될 때 자식 컴포넌트가 변경되지 않았다면, 해당 자식 컴포넌트는 다시 렌더링되지 않도록 도와준다.

memo (React.memo) 사용법

export default React.memo(Box1);
  • 예시 코드
//App.jsx
import { useState } from "react";
import "./App.css";
import Box1 from "./components/Box1";
import Box2 from "./components/Box2";
import Box3 from "./components/Box3";

function App() {
  const [count, setCount] = useState(0);
  console.log("App 컴포넌트가 렌더링 되었어요!");
  //1증가
  const onPlusButtonClickHandler = () => {
    setCount(count + 1);
  };
  //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.jsx
import React from "react";

function Box1() {
  const style = {
    width: "100px",
    height: "100px",
    backgroundColor: "#01c49f",
    color: "white",
  };
  console.log("Box1 컴포넌트가 렌더링 되었어요!");
  return <div style={style}>Box1</div>;
}
export default React.memo(Box1);


//Box2.jsx
import React from "react";

function Box2() {
  const style = {
    width: "100px",
    height: "100px",
    backgroundColor: "#4e93ed",
    color: "white",
  };
  console.log("Box2 컴포넌트가 렌더링 되었어요!");
  return <div style={style}>Box2</div>;
}

export default React.memo(Box2);

//Box3.jsx
import React from "react";

function Box3() {
  const style = {
    width: "100px",
    height: "100px",
    backgroundColor: "#c491be",
    color: "white",
  };
  console.log("Box3 컴포넌트가 렌더링 되었어요!");
  return <div style={style}>Box3</div>;
}
export default React.memo(Box3);

React.memo와 리렌더링 이슈:

  • React.memo를 사용하여 Box1.jsx를 메모이제이션했지만, 리렌더링이 발생한 이유는 부모 컴포넌트인 App.jsx가 리렌더링되면서 함수가 다시 만들어지기 때문임

useCallback의 역할:

  • useCallback은 함수를 메모이제이션하여, 함수가 변경되지 않도록 함
  • 부모 컴포넌트인 App.jsx에서 initCount 함수를 자식 컴포넌트 Box1.jsx로 전달하고 있음
const initCount = useCallback(() => {
  console.log(`${count}에서 0으로 변경되었습니다.`);
  setCount(0);
}, [count]);

☞ 의존성 배열[]이 필요한 이유 : 함수가 참조하는 외부 변수가 있다면, 해당 변수를 의존성 배열에 추가하여 그 값이 변경될 때만 함수가 다시 만들어지도록 함

결론: useCallback을 사용하면 함수가 메모이제이션되어, 같은 함수가 반복해서 생성되지 않고 필요한 경우에만 새로 생성되어 성능을 최적화할 수 있음

###UseMemo

  • Memoization 기법을 사용하여 동일한 계산을 반복하지 않고 결과를 캐싱하는 React Hook.
  • 렌더링 중에 불필요한 계산을 피하고 성능을 향상시키는데 사용됨.

###UseMemo 사용법

  • useMemo 함수를 사용하여 특정 값을 memoization하고, 해당 값이 변경될 때만 다시 계산함
const value = useMemo(()=> {
	return 반환할_함수()
}, [dependencyArray]);

사용 예시: const value = useMemo(() => calculateValue(), [dependencyArray]);

useMemo를 적용하면 좋은 경우
1. 무거운 계산이 필요한 컴포넌트에서 useMemo를 사용하여 계산 결과를 캐싱해서 불필요한 렌더링을 피하고 성능 향상.

  1. useEffect 내에서 객체의 변화를 감지할 때, 불필요한 렌더링이 발생하는 문제 해결을 위해 useMemo 활용.

! 주의사항

  • useMemo를 남용하면 메모리 소비가 늘어날 수 있으므로 필요한 경우에만 사용해야 함.
  • 성능 최적화를 위해 신중하게 사용.

###. 생명주기(LifeCycle)

  • 컴포넌트의 생성, 업데이트, 소멸과 같은 단계를 나타내는 개념.
  • 리액트 컴포넌트는 Mount(생성), Update(업데이트), Unmount(소멸)의 세 가지 주요 생명주기를 갖고 있음.
  1. Mount (생성)
  • 컴포넌트가 처음 생성될 때의 생명주기 단계.
  • 다양한 메서드들이 존재함.
    constructor(),
    getDerivedStateFromProps(nextProps,revState)
    render()
    componentDidMount()

1) 각 메서드 소개
constructor:
컴포넌트가 처음 만들어질 때 호출되는 생성자 메서드.

getDerivedStateFromProps:
부모 컴포넌트로부터 전달받은 props를 사용하여 state를 설정하는 역할을 하는 메서드.

render:
컴포넌트를 렌더링하는 메서드.

componentDidMount:
컴포넌트가 브라우저에 표시된 후 호출되는 메서드.

  1. Update (업데이트)
  • 컴포넌트가 업데이트되는 생명주기 단계.
  • 리렌더링이 발생하는 경우 해당 단계에 진입함.
    1) 각 메서드 소개
    getDerivedStateFromProps:
    마운트 과정에서도 호출되었던 메서드로, 업데이트 시에도 호출됨.

shouldComponentUpdate:
리렌더링 여부를 판단하는 메서드. true인 경우 리렌더링 진행, false인 경우 리렌더링 하지 않음.

render:
변경사항 반영이 준비된 후 호출되는 렌더링 메서드.

getSnapshotBeforeUpdate:
컴포넌트에 변화가 일어나기 직전 DOM의 상태를 저장하는 메서드.

componentDidUpdate:
컴포넌트 업데이트 작업이 완료된 후 호출되는 메서드.

  1. Unmount (소멸)
  • 컴포넌트가 DOM에서 제거되는 시점의 생명주기 단계.
    1) 각 메서드 소개
    componentWillUnmount:
    컴포넌트가 사라지기 전에 호출되는 메서드.
    useEffect의 return과 동일한 역할을 수행함.

[부연설명] 함수형 컴포넌트에서는 클래스형 컴포넌트의 생명주기 메서드를 대체하기 위해 useEffect를 사용함.

Dom과 가상 Dom(Virtual Dom)

  • 리액트와 뷰는 가상돔(Virtual DOM)을 사용하여 웹 페이지를 그린다.
  • 가상돔은 효율적인 알고리즘을 사용하여 화면을 그리기 때문에 성능이 우수하다.

DOM

  • DOM(Document Object Model)은 웹 페이지의 구조를 tree 형태로 표현한 것
  • 각 노드는 해당 요소에 접근하고 조작할 수 있는 API를 제공함
  • DOM을 사용하여 HTML 요소에 접근하고 수정할 수 있음

가상DOM(Virtual DOM)

  • 리액트는 실제 DOM을 효율적으로 조작하기 위해 가상DOM을 활용함
  • 메모리에 저장된 실제 DOM의 복사본으로, 객체 형태로 존재하여 더 빠르게 조작이 가능함
  • 화면이 갱신되면 가상DOM을 사용하여 변경된 부분을 파악하고, Batch Update를 통해 한 번에 적용함

DOM 조작 과정

  • 리액트는 항상 두 가지 버전의 가상DOM을 가지고 있음(화면이 갱신되기 전과 후의 가상DOM 객체)
  • State가 변경되면 새로운 가상DOM을 생성하고, 이전 가상DOM과 비교하여 변화를 파악함
  • 변화가 있는 부분만을 최적화된 방식으로 실제 DOM에 적용함

Batch Update

  • 리액트는 변경된 엘리먼트를 한 번에 모아 Batch Update 방식으로 화면에 반영한다.
  • 여러 번의 화면 갱신이 필요한 경우에도 최소한의 갱신만이 이루어진다.

클릭 한 번으로 화면에 있는 5개의 엘리먼트가 바뀌어야 한다면?

  • 실제 DOM : 5번의 화면 갱신 필요
  • 가상 DOM : Batch Update로 인해 단 한번만 갱신 필요
    출처 : 내배캠 강의자료

Redux

  • 리덕스는 중앙 state 관리소를 사용할 수 있게 도와주는 패키지로, 전역 상태를 효과적으로 관리하는 라이브러리
  • 프론트엔드 개발자들은 리덕스를 "전역 상태 관리 라이브러리"로 표현함 - useState로 관리되는 Local State의 불편함을 일부 해소됨

Redux 필요성

  1. useState의 불편함
  • Props로 State를 전달하는 것은 부모-자식 간의 관계가 필요하고, 중간 컴포넌트를 거쳐야 함
  • 리덕스를 사용하면 부모 관계 없이 State를 전달할 수 있으며, 자식 컴포넌트에서 생성한 State를 부모에서도 사용 가능함
  1. Global state와 Local state
    1) Local State (지역 상태)
    정의 : 컴포넌트 내부에서 useState를 통해 생성되고 관리되는 상태.
    범위 : 해당 컴포넌트 내에서만 유효하며, 다른 컴포넌트에서 직접 접근할 수 없음.

특징:

  • 좁은 범위에서만 사용되며 해당 컴포넌트의 상태를 관리하는 데에 활용됨.
  • 해당 컴포넌트가 언마운트되면 상태 정보가 소멸되고, 재렌더링 시 초기화됨.
  • 주로 간단한 UI나 컴포넌트 내부의 로컬 상태를 다룰 때 활용됨.

2) Global State (전역 상태)
정의: 중앙 state 관리소(예: 리덕스)에서 생성되고 관리되는 전역적인 상태.
범위: 모든 컴포넌트에서 접근 가능하며, 중앙 state 관리소를 통해 상태를 공유함.

특징:

  • 어떤 컴포넌트에서든지 해당 전역 상태에 접근하여 값을 읽고 갱신할 수 있음.
  • 컴포넌트 간의 데이터 공유와 상태 관리를 위해 사용됨.
  • 컴포넌트의 생명주기와 독립적으로 유지되며, 어플리케이션 전체에서 일관된 상태를 유지할 수 있음.
  • 주로 복잡한 상태 관리와 여러 컴포넌트 간의 데이터 교환에 활용됨.

3) Global state와 Local state의 간단한 비교
Local State: 한 컴포넌트 내에서만 사용되는 상태로, 해당 컴포넌트의 내부에서만 유효하다.
Global State: 어플리케이션 전체에서 사용되는 상태로, 모든 컴포넌트에서 접근 가능하며 중앙 state 관리소를 통해 관리된다.

Redux 사용법 - 1. 리덕스 설정

  1. 두개의 패키지를 설치해야함
    1) redux
    2) react-redux : 리덕스를 리액트에서 사용할 수 있도록 서로 연결하는 패키지
yarn add redux react-redux

//아래와 같은 의미
yarn add redux
yarn add react-redux
  1. 폴더 구조 만들기
    1) src 폴더 내 redux 폴더 만들기
    2) redux 폴더 내 config와 modules 폴더 만들기
    3) config 폴더 안에 configStore.js 만들기
    4) modules 폴더 안에 counter.js 만들기

redux : 리덕스 관련 모든 코드가 담긴 폴더
config : 리덕스 설정 관련 모든 파일
configStore : 중앙 state 관리소 => 설정 코드(js)
modules : 중앙 state에서 관리하는 state의 그룹

Redux 사용법 - 2. 설정코드 작성

1) 설정 코드 작성 시, 주의사항

  • 설정 코드는 리덕스 사용 방법에 중점을 두어 이해할 필요가 없는 코드임
  • 리덕스 사용 방법에 집중하여 학습한다.

2) src/configStore.js
-스토어를 생성하는 핵심 메소드인 createStore를 이용하여 스토어를 만든다.

  • combineReducers를 사용하여 여러 개의 리듀서를 하나의 상태 객체로 만든다.
  • 생성한 스토어를 내보내어 다른 모듈에서 사용할 수 있도록 한다.
//src/configStore.js

//중앙 데이터 관리소를 설정하는 방법
import { createStore } from "redux";
import { combineReducers } from "redux";

const rootReducer = combineReducers({

});
const store = createStore(rootReducer);

export default store;

3) index.js

  • Provider를 이용하여 리액트 애플리케이션을 감싸주고, 생성한 스토어를 제공한다.
//index.js
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { Provider } from "react-redux";
import store from "./redux/config/configStore";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

4) 모듈 counter
(1) initialState === 초기 상태값
모듈에서 사용할 초기 상태값을 정의한다.

(2) Reducer === 변화를 일으키는 함수
리듀서는 변화를 일으키는 함수로, 모듈의 상태를 어떻게 변경할지 정의한다.

(3) 카운터 모듈을 스토어에 연결하기
생성한 모듈을 스토어와 연결하여 사용할 수 있도록 설정한다.

//중앙 데이터 관리소를 설정하는 방법

import { createStore } from "redux";
import { combineReducers } from "redux";
import counter from "../modules/counter"; // <<<여기

const rootReducer = combineReducers({
  counter, // <<<여기
});
const store = createStore(rootReducer); 

export default store;

5) 스토어와 모듈 연결하기
(1) useSelector = 스토어 조회
useSelector 훅을 사용하여 컴포넌트에서 필요한 상태값을 컴포넌트에서 조회할 수 있다.

import logo from "./logo.svg";
import "./App.css";
import { useDispatch, useSelector } from "react-redux"; //
function App() {
  const counter = useSelector((state) => {
    return state.counter;
  });

  const dispatch = useDispatch();
  return (
    <div className="App">
      <div>현재 카운트 : {counter.number}</div>
    </div>
  );
}

export default App;

Counter.js 모듈의 State + - 버튼 눌러서 변경하기

1) State 변경: 리덕스에서 state를 변경하려면 dispatch를 통해 액션 객체를 리듀서로 보내야 함.
2) 액션 객체 생성: 액션 객체는 type을 가진 객체로, 리듀서에 명령을 전달하는 역할.
3) Dispatch: useDispatch 훅을 이용하여 액션 객체를 리듀서로 보내는 함수 생성.
4) 액션 객체 수신(받기): 리듀서에서 console.log(action) 등으로 액션 객체를 확인 가능.

5) 리듀서 로직 구현: 액션 객체의 type에 따라 state 변경 로직을 리듀서에 구현

요약

  1. 액션객체
    반드시 type이라는 key를 가져야 하는 객체로, 리듀서에게 전달되어 상태를 변경하는 명령을 포함한다.

  2. 디스패치
    액션 객체를 리듀서로 보내는 함수로, useDispatch 훅을 사용하여 컴포넌트 내에서 생성한다.

  3. 리듀서
    디스패치를 통해 전달받은 액션 객체를 검사하고, 조건에 따라 새로운 상태값을 생성하는 함수이다.

  4. 대문자
    액션 객체의 type 값은 대문자로 작성하는 것이 관례이다.

오늘의 한줄평 : 역시 리덕스가 어렵다고 하던데 역시, 조금 어려웠다.

profile
귀여운 걸 좋아하고 흥미가 있으면 불타오릅니다💙 최근엔 코딩이 흥미가 많아요🥰

0개의 댓글