[React] 기본 지식 정리

Muru·2024년 11월 13일

[React] 지식 저장소

목록 보기
1/30
post-thumbnail

1 : React 개념과 특징들

React는 JavaScript 라이브러리로, 주로 사용자 인터페이스(UI)를 구축하는 데 사용됩니다.
React는 단일 페이지 애플리케이션(SPA)이나 모바일 애플리케이션에서 사용자 인터페이스의 동적인 변화를 관리하는 데 특화되어 있습니다.

라이브러리는 미리 작성된 코드 묶음으로, 특정 기능을 쉽게 사용할 수 있도록 제공하는 코드 집합입니다. 개발자는 필요한 기능을 직접 작성하지 않고, 라이브러리를 호출하여 사용할 수 있습니다.

반면에 vue는 프레임워크로 동작하여 앱을 구축할 수 있는 모든 기능이 내장되어 있습니다. 구조가 비교적 정해져있어 일관된 방식에서 개발하기 쉽습니다.
React가 vue에 비해서 난이도가 있을수 있지만 자유도가 높아 세밀한 설계가 가능합니다.

React 주요 특징

  • 재사용 가능한 컴포넌트로 나누어 개발한다.
  • 가상 DOM을 사용하여 먼저 반영하고, 이전 상태와 비교해 최소한의 DOM만을 조작하여 성능을 최적화한다
  • 단방향 데이터 흐름이다. 데이터 부모에서 자식으로 흘러가며 이로써 버그 추적과 유지보수가 쉬워진다.
  • JSX(JavaSCript XML)을 사용하여 HTML같은 구문을 JavasCript 코드안에서 사용할 수 있다. const element = <h1> 안녕 </h1>;
  • Reack Hooks을 사용하여 함수형 컴포넌트에서도 상태를 관리하고 생명주기 메서드를 활용 할 수 있다. (useState, useEffect, useContext ... )

가상돔이란?
가상돔이란 실제 DOM과 유사한 구조를 가진 경량화된 JS 객체로 , React가 효율적으로 UI를 업데이트를 하기 위해 사용되는 핵심 개념입니다. React의 성능 최적화와 직결됩니다. 실제 DOM은 무겁기 때문에 최소한의 조적으로 성능을 극대화 하기 위함입니다. 다음과 같은 작동 원리를 가집니다.

  1. 가상 돔 트리를 생성합니다.
  2. State나 Props가 변경시 새로운 가상 돔 트리를 생성합니다
  3. Diffing 알고리즘을 적용하여 두 개의 가상 DOM을 비교 합니다.
  4. 만약 변경된 부분이 있을 경우 해당 변경된 부분만 실제 DOM에 적용합니다.
  • 다른 vue.js등 프레임워크들도 가상 DOM과 유사한 개념을 사용합니다. 다만 React는 Diffing 알고리즘 덕에 최적화된 업데이트를 제공합니다.

2 : 리액트에서 컴포넌트를 생성하는 방법

리액트에서 컴포넌트를 생성하는 방법은 클래스형 컴포넌트와 함수형 컴포넌트가 있습니다.

클래스형 컴포넌트는 ES6의 class를 사용하여 정의합니다. 다음과 같은 특징이 있습니다.

  • 우리가 알고있는 React Hooks은 사용하지 못합니다.
  • 대신 상태관리는 this.state, this.setState 생명 주기 관리는 생명주기 메서드(componentDidMount)를 사용합니다.
  • Hooks을 사용하는 함수형 컴포넌트 대비 코드가 길고 복잡합니다.

함수형 컴포넌트는 함수로 작성된 컴포넌트 입니다. 다음과 같은 특징이 있습니다.

  • 상태나 생명주기관리를 React Hooks를 사용해 매우 간결한 문법과 성능상의 장점을 가집니다.
  • 클래스형 컴포넌트보다 가볍고, 초기 렌더링과 업데이트시 성능에 유리합니다.

3 : 컴포넌트의 생명 주기

React의 컴포넌트 생명 주기는 컴포넌트가 생성되고 업데이트되며 제거되는 과정에서 호출되는 일련의 메서드와 훅을 말합니다.

클래스형 컴포넌트를 사용하던지, 함수형 컴포넌트를 사용하던지 생명주기를 관리방식은 다를지언정 모두 동일한 생명주기 단계를 가집니다.

React 컴포넌트는 생성되며 사라질때까지 다음과 같은 공통 단계를 거칩니다.

  • 마운트 : 컴포넌트가 처음 DOM에 추가되는 단계
  • 업데이트 : 컴포넌트의 props나 state가 변경되어 다시 렌더링 되는 단계
  • 언마운트 : 컴포넌트가 DOM에서 제거되는 단계

생명주기 관리 방식은 다음과 같이 다르게 관리됩니다.
함수형 컴포넌트에서는 React Hooks를 사용하여 생명 주기를 관리합니다.
특히 useEffect훅을 사용하여 상태 변경 및 외부 자원 변경시 필요한 작업을 수행합니다.
클래스형 컴포넌트에서는 고유의 생명 주기 메서드를 사용하여 상태 변경, 데이터 로드, 정리 작업등을 수행 합니다.

클래스형 컴포넌트의 고유 메소드를 활용하여 생명 주기를 보겠습니다.

마운트 : 컴포넌트가 처음 DOM에 추가되는 단계
constructor() : 해당 메서드로 상태를 정의 할 수 있습니다.
getDerivedStateFromProps() : props로 받아온 값이 상태에 영향을 미친다면 반영하도록 수행합니다.
render() : 컴포넌트를 렌더링 합니다.
componentDidMount() : 컴포넌트가 렌더링이 된 이후에 실행되는 함수로 주로 비동기 작업을 할때 사용합니다.

업데이트 : 컴포넌트의 props나 state가 변경되어 다시 렌더링 되는 단계
getDerivedStateFromProps() : 컴포넌트의 props나 상태가 바뀔때 호출 됩니다.
shouldComponentUpdate() : 컴포넌트의 State나 props가 변경되었을때 리렌더링을 할것인지 말것인지 결정합니다.
render() : 컴포넌트를 리렌더링 합니다.
getSnapshotBeforeUpdate() : 컴포넌트의 변화를 DOM에 적용하기 전 실행되는 함수로 변화하기전 상태를 참고해야 할 경우 사용합니다.
componentDidUpdate() : 리렌더링 작업이 끝난후 실행되는 함수 입니다. 추가로 직전의 가졌던 데이터에 접근이 가능합니다.

언마운트 : 마운트의 반대과정으로, 컴포넌트를 DOM에서 제거합니다.
componentWillUnmount() : 언마운트 하기전에 해당 메소드가 실행됩니다.
등록한 이벤트,타이머가 있다면 여기서 제거해야합니다. 안그러면 메모리 누수 발생 가능성이 생깁니다. ( 페이지를 이동해도 이벤트 리스너가 남아있는등.. )

4 : 함수형 컴포넌트에서 클래스형 컴포넌트의 라이프 사이클 메소드를 비슷하게 사용하는 방법

  1. this.state, setState는 상태를 저장하는 객체로
    이는 useState로 비슷하게 사용 가능합니다.

  2. ComponentDidMount는 렌더링 직후 한번만 실행 하는 메소드로
    이는 useEffect에서 의존성 배열을 빈배열로 만들면 비슷하게 동작합니다.

  3. ComponentDidUpdate는 리렌더링 될때마다 실행 되는 메소드로 이때 상태를 비교하여 바뀌었을경우 실제 DOM에 반영합니다.
    이는 useEffect에서 의존성 배열에 상태를 넣어주면 비슷하게 동작합니다.

  4. ComponentWillUnMount는 컴포넌트가 언마운트 되기전에 실행되는 메소드로, 주로 정리 작업을 수행합니다(클린업 함수).
    이는 useEffect내에서 return문을 서술해주면 비슷하게 동작합니다.

  5. shouldComponentUpdate는 컴포넌트가 리렌더링 될것인지 결정하는 메소드 입니다. 해당 메소드를 자동으로 구현한것이 React.PureComponent컴포넌트 입니다.
    React.PureCompoent를 사용하면 props나 state의 변경이 이루어질때만 리렌더링이 이루어지도록 합니다.

    React.PureComponent를 사용하지않는 일반 컴포넌트의 경우 부모의 리렌더링이 이루어질경우 자식 컴포넌트도 props가 어떤지 상관없이 필연적으로 리렌더링이 발생하게됩니다. 불필요한 작업인데도 말이죠.

이는 React.memo로 비슷하게 동작합니다.

5 : 다양한 useHook

함수형 컴포넌트에서 쓰이는 useHook들은 종류도 많습니다. 하나씩 쓰임새를 서술 해보겠습니다.

useState : 상태를 관리할 수 있게 해주는 훅입니다. 로컬 상태를 정의하는데 필수적인 훅으로 복잡한 상태라면 useReducer를 사용하는게 좋습니다.


useReducer : 복잡한 상태 관리 로직을 관리할 때 유용합니다.
Reducer 함수를 정의하고 액션을 받아 상태를 업데이트 합니다. 코드의 복잡성을 줄이고 가독성을 높일 수 있습니다.

/* useReducer의 주된예시 : 로그인 폼 관련 */
const initialState = {
    email: '',
    password: '',
    error: null,
    isSubmitting: false,
};

function formReducer(state, action) {
    switch (action.type) {
        case 'SET_EMAIL':
            return { ...state, email: action.payload };
        case 'SET_PASSWORD':
            return { ...state, password: action.payload };
        case 'SET_ERROR':
            return { ...state, error: action.payload };
        case 'SET_SUBMITTING':
            return { ...state, isSubmitting: action.payload };
        default:
            return state;
    }
}

function LoginForm() {
    const [state, dispatch] = useReducer(formReducer, initialState);

useContext : useContext는 전역적으로 상태를 공유 할 수 있는 훅으로, 여러 컴포넌트에서 공유해야 하는 데이터를 효율적으로 제공합니다.
부모 컴포넌트 => 자식 컴포넌트1 => 자식컴포넌트2 => 자식컴포넌트3
이와같이 프로젝트가 props를 전달하면서 자식컴포넌트3만 props를 사용하고 자식 컴포넌트1,2는 해당 props를 쓰지 않는 구조라면어떨까요? 매우 비효율적일것 입니다.
이를 해결하기 위해 전역적 상태공유를 설계해주는 훅이 useContext 훅 입니다.

context API(useContext)는 상태 관리가 아닙니다. 상태 공유에 가깝습니다.
주로 props drilling을 해결하고자 많이 사용합니다.

상태관리를 할 수 있으려면 다음 세가지를 충족 해야 합니다.

  • 초기상태 저장
  • 상태 업데이트
  • 현재 상태 읽기
    useState, useReducer, Redux 라이브러리는 상태를 관리 할 수 있습니다

useState : 단순하고 독립적인 상태를 관리해야 하며, 상태가 한 컴포넌트 내에서만 사용되거나, 간단한 부모-자식 관계에서 공유될 때 사용 합니다.
useReducer : 상태가 복잡하고 관련 로직이 많으며, 상태 변경이 다양한 액션에 의해 이루어질 때 사용.
Redux 프로젝트의 상태가 복잡하고 전역적으로 공유 및 관리되어야 하며, 상태 흐름을 체계적으로 추적하고 싶은 경우 사용합니다.
다만 보일러 플레이트가 많고 러닝커브가 존재합니다. 보일러 플레이트는 리덕스 툴킷을사용하여 어느정도 해소가 가능합니다.

Step1 : Context 생성 ( CountContext.js )

import { createContext } from "react";
const countContext = createContext();
export default countContext;

Step2 : Provider 설정 ( App.js )

import React, { useState } from "react";
import CountContext from "./CountContext";
import Child1 from "./Child1"
const App = () => {
  const [count, setCount] = useState(0);

  const increment = () => setCount(count+1)
  return (
    <CountContext.Provider value={{count, increment}}>
      <h1>부모컴포넌트</h1>
      <Child1/>
    </CountContext.Provider>    
  );
};

export default App;

Step3 : Context 값 사용 ( Child1을 거치지 않고 곧바로 Child2.js)

import React, { useContext } from "react";
import CountContext from "./CountContext";

function Child2() {
  const { count, increment } = useContext(CountContext);

  return (
    <div>
      <h2>Child2 Component</h2>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}


export default Child2;

실제로 Child1은 그 어느 props를 전달받지 않았다.

// Child1.js
import React from 'react';
import Child2 from './Child2';

function Child1() {
  return (
    <div>
      <h2>Child1 Component</h2>
      <Child2 />
    </div>
  );
}

export default Child1;


useMemo : 값을 메모이제이션하여 성능을 최적화하는 데 사용하는 훅 입니다. 특정 값이나 연산 값을 캐싱하여, 의존성이 변경할때만 다시 계산하고 그 외에는 이전 결과를 재사용합니다.


-useMemo를 쓰지 않는경우-

const MyComponent = ({ items }) => {
  // useMemo를 사용하지 않아서 매번 정렬을 수행해야한다.
  // 시간복잡도가 클경우 성능에 불리함을 줄 수 있다.
  const sortedItems = items.sort((a, b) => a.value - b.value);

  return (
    <div>
      {sortedItems.map((item) => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
};
-useMemo를 쓴 경우-

const MyComponent = ({ items }) => {
  // useMemo를 사용하여 items가 변경될 때만 정렬
  const sortedItems = useMemo(() => {
    return [...items].sort((a, b) => a.value - b.value);
  }, [items]);

  return (
    <div>
      {sortedItems.map((item) => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
};

useCallback : useMemo와 비슷하지만 useCallback훅은 함수의 참조를 메모이제이션하여, 컴포넌트가 리렌더링 될때마다 불필요하게 함수가 다시 생성되는것을 방지합니다.

useCallback을 사용하지않고 부모 컴포넌트에서 작성된 함수를 자식 컴포넌트에게 props로 전달할때 생각해보겠습니다.
1. 부모 컴포넌트가 리렌더링되어 함수참조 변경
=>
2. 자식 컴포넌트로 변경된 함수참조를 전달
=>
3. 자식 컴포넌트는 함수참조(props)가 바뀐것으로 인식하여 리렌더링 발생

부모 컴포넌트에서 함수를 useCallback로 감싸면 어떻게 될까요??
1. 부모 컴포넌트의 리렌더링 발생 (함수참조는 그대로)
=>
2. 자식 컴포넌트로 변경되지않은 함수참조 전달
=>
3. 자식 컴포넌트는 함수참조가 바뀌지 않은것으로 인식하여 리렌더링이 발생하지 않음


번외로 비슷한 최적화 기법으로써 React.memo()가 있는데요.
React.memo()는 props가 변하지 않으면 자식 컴포넌트는 리렌더링되지 않게 합니다.
React.memo()는 HOC로 클래스형 컴포넌트와 함수형 컴포넌트 둘다 사용이 가능하지만
useMemo()는 훅으로써 함수형 컴포넌트에서만 사용이 가능합니다. 이를 클래스형 컴포넌트에서 명시적으로 대칭 가능한 메서드는 없습니다.

리액트의 렌더링 성능 향상을 위해서는 위 세가지 방법을 생각해보면 좋습니다!


useEffect : 함수형 컴포넌트에서 부수 효과를 수행하기 위해 사용하는 훅 입니다. 데이터를 가져오거나, DOM 조작, 타이머 설정 등과 같은 작업을 useEffect에서 할 수 있습니다.

부수 효과 : 컴포넌트의 렌더링과 직접적으로 관련이 없는 작업을 의미하며, 보통 렌더링이 완료된 후에 수행해야 하는 작업 입니다.

클래스형 컴포넌트에서 componentDidMount, componentDidUpdate, componentWillUnmount 이 메소드들을 함수형 컴포넌트에서는 useEffect를 사용하여 유사하게 구현이 가능합니다.

componentDidMount는 최초 마운트이후 한번 실행되는 함수인데요.
함수형 컴포넌트에서는 의존성 배열을 빈배열[]로 설정하여 컴포넌트가 마운트될때 한 번만 실행 하도록 할 수 있습니다.

  • 컴포넌트가 첫 마운트될때 서버에서 데이터를 가져와 상태로 저장하는 작업에 사용 할 수 있습니다.
  • 컴포넌트가 마운트될때 이벤트 리스너를 등록하고, 클린업 함수(componentWillUnmount)를 통해 컴포넌트가 언마운트될때 이를 제거할 수 있습니다.

componentWillUnmount는 언마운트가 되기 전에 실행되는 함수인데요.
함수형 컴포넌트에서는 useEffect에서 return문 내에 이벤트 리스너를 제거하거나 , 타이머 해제, 구독 해지와같은 클린업 작업을 수행할때 사용됩니다.

componentDidUpdate는 컴포넌트가 업데이트 될때마다 호출되는 함수인데요. 이와 비슷하게 useEffect에서는 의존성 배열을 통해 상태나 props가 변경될때마다 추가 작업을 수행할 수 있도록 사용 됩니다.


useLayoutEffect는 useEffect와 유사한 기능이지만 뜯어보면 많이 다릅니다.
useEffect 훅은 비동기적으로 작동하여 브라우저의 렌더링 과정을 모두 마친다음에 실행되지만, useLayoutEffect 는 페인팅 이전에 실행되고 동기적으로 작성하므로 렌더링 속도에 영향을 미칠 수 있습니다.

참고 : 브라우저 렌더링 과정

  1. DOM (Document Object Model) 및 CSSOM(CSS Object Model) 생성
  2. 렌더 트리 생성
  3. 레이아웃
    >>> useLayoutEffect 실행 시점 <<<
  4. 페인팅
  5. 합성
    >>> use Effect 실행 시점 <<<

그럼 useLayoutEffect를 사용해야하는 이유는 뭘까요? 쉽게 생각하면 됩니다. 우리가 눈으로 보여지기전에 해야할 작업을 수행하고싶을때 사용하면됩니다.

  • 레이아웃 측정 및 동적 스타일 적용
  • 렌더링 후 특정 요소로 스크롤이 되어있도록 적용
  • 렌더링 후 특정 입력 필드에 포커스를 설정해야 할 때 적용
  • 상태변화로 인해 DOM을 업데이트할경우 깜빡임 막기

단, 데이터를 읽어오기 같은 비동기 작업들은 페인팅이 지연 될 수 있고 React의 흐름에도 맞지않으므로 조심해야 합니다.

예시를 들어보겠습니다.

const [age, setAge] = useState(0);

  useEffect(() => {
    if(age ===0 ) {
      setAge(Math.ceil(Math.random() * 100))
    }
  }, [age]);
  
  return (
    <>
      <div style={{width:"200px", height:"50px", backgroundColor:"gray"}}>{`나는 ${age}살이야. 앞으로 잘부탁해`}</div>
      <button onClick={() => setAge(0)} style={{width : "200px", height:"200px"}}>나이먹기</button>
    </>
  );
  

위 코드를 실행하면 다음과 같이 깜빡임 현상이 일어납니다.
버튼 클릭 => age가 0으로 바뀜 => 화면에 0이 표시됨 => useEffect 실행 => random age 설정

무조건 0으로 바뀌는것을 보기 싫다면 상태변화 적용을 페인팅이전에 적용시키면됩니다!
간단히 useEffect => useLayoutEffect로 바꿔주면 됩니다.


useRef는 React 훅으로, 렌더링에 필요하지 않은 값을 저장하거나 특정 DOM 요소를 참조하는 데 사용됩니다. 반환된 객체는 컴포넌트의 전 생애주기 동안 유지됩니다.

주요 특징은 다음과 같습니다

  • useRef는 리렌더링 없이 값을 유지 합니다.
  • .current 속성을 통해 DOM 요소나 값을 참조합니다.
  • 컴포넌트가 다시 렌더링되어도 참조한 값은 유지됩니다.

주 사용 사례는 다음과 같습니다

  • DOM 조작 : 특정 요소에 포커스를 설정하거나 크기, 위치를 읽어야 할 때.
  • 렌더링 방지 : 상태 변경과 무관한 값을 저장할 때
    예를들어, useState로 설정한 입력폼은 한글자 한글자 입력할때마다 쓸데없는 리렌더링이 됩니다. (단, 이경우도 디바운스 및 쓰로틀링으로 조절이 가능하긴 함. 경우에 따라서 써야함)
    반면에, useRef로 설정한 입력폼의 경우 리렌더링 없이 값만 저장하고 제출할수 있습니다.
  • 비동기 작업의 ID관리 : setTimeout()함수같은 비동기함수를 사용할때 타이머ID를 통해 식별을 하는데요. 이 타이머ID는 실제 렌더링을 하지 않으므로 굳이 useState로 저장할 이유가 없습니다.

useImperativeHandle은 특정 메서드나 값을 부모에게 노출할 수 있습니다. forwardRef와 함께 사용합니다.
부모가 ref를 통해 자식 컴포넌트의 특정 메서드나 속성을 직접 호출하여 사용 할 수 있습니다.
하지만 해당 훅은 공식문서에서도 사용을 지양하라고 나와 있습니다. 선언형 방식인 리액트와는 다른 결을 가진 명령형 방식이라 그렇습니다. (아직 써본적이 없긴함..)

6 : 상태 관리 라이브러리

상태가 복잡하거나 전역적으로 사용해야 할 때 또는 서버의 데이터를 상태로 쉽게 처리 할 수 있게 도와주는 상태 관리 라이브러리가 있습니다.
Redux, Recoil, Mobx, Zustand, Jotai, ReactQuery

각각의 장단점이 모두 있지만 세 가지를 두고 보겠습니다 Redux, Zustand, ReactQuery

6.1 : Redux

Redux : 상태를 한 곳에서 관리하는 Flux 아키텍처 기반의 라이브러리 입니다.

Flux 아키텍쳐는 데이터 흐름을 단방향으로 관리하는것을 중심으로 설계합니다.
1. 사용자가 액션을 발생시킵니다.
2. 디스패쳐를 통해 액션을 전달합니다.
3. 스토어는 디스패쳐를 통해 액션을 받았습니다.
(액션에 따라서 상태를 업데이트 합니다.)
4. View 업데이트
(스토어의 상태가 변경되면 View가 업데이트 됩니다.)

알려져있듯이 많은 보일러 플레이트와 러닝 커브가 있음에도 데이터 흐름이 명확하여 디버깅과 유지보수가 쉽습니다. 그래서 중,대규모 어플리케이션에 적합합니다.하지만, 리덕스 툴킷을 사용하여 보일러 플레이트를 줄인다 해도 여전히 많은 코드를 써야 하기 때문에 소규모 어플리케이션을 사용한다고 하면 굳이 Redux를 사용해야할지는 개발자의 선택에 달려있습니다.

아래는 리덕스 사용 예시입니다.
App.js store/counterSlice.js, store/store.js로 간단하게 예제를 확인해봅시다.

counterSlice에서 createSlice를 통해 간단히 상태와 액션을 정의합니다.
App.js에서 useSelector로 상태를 구독하며 , useDispatch로 액션을 호출합니다.
store.js는 상태가 저장되는곳입니다

  • 상태를 저장합니다.
  • 리듀서를 등록합니다.
  • 액션 디스패치 처리를 합니다.

App.js ( 전역 상태를 사용하는곳 )

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment,decrement,increase,toggleCounter } from './redux/store/counterSlice';

const App = () => {
  const count = useSelector((state) => state.counter.counterState);
  const showCounter = useSelector((state) => state.counter.showCounter);

  const dispatch = useDispatch();

  return (
    <div style={{ textAlign: 'center', marginTop: '50px' }}>
      {showCounter ? <h1>Counter: {count}</h1> : null}
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(increase(10))}>Increment</button>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
      <button onClick={() => dispatch(toggleCounter())}>토글버튼</button>
    </div>
  );
};

export default App;

store.js (중앙 저장소)

// index.js의 역할 =>
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";

const store = configureStore({
  reducer: {
    counter : counterReducer,
  }
});

export default store;

counterSlice.js (상태와 액션을 정의)

import { createSlice } from "@reduxjs/toolkit";

const initialState = { counterState : 0, showCounter : true};

// createSlice 함수(세 가지 주요 프로퍼티를 받는다.)
const counterSlice = createSlice({
    // 식별자 역할을 하는 name 프로퍼티
    name : 'counter',
    // 초기값 역할을 하는 inintialState 프로퍼티
    initialState,
    // 리듀서 함수의 역할을 하는 reducers 프로퍼티
    reducers : {
        increment(state) {
        state.counterState++;
        },
        decrement(state) {
        state.counterState--;
        },
        // createSlice를 사용할경우 액션은 정해진 프로퍼티명인 payload를 사용해야함
        increase(state,action) {
            state.counterState = state.counterState + action.payload
        },
        toggleCounter(state) {
            state.showCounter = !state.showCounter;
        }

        // reducer 메소드가 내부적으로있음.
        /*createSlice에 서술되어있는 모든 프로퍼티가 담겨져있음. */
        // actions 메소드가 내부적으로있음.
        /* 액션 생성자들이 포함된 객체가 담겨져있음. */
    },

    
});

// 액션 생성자들이 포함된 객체를 내보낸다.
export const {increment,decrement,increase,toggleCounter} = counterSlice.actions;

// counterSlice의 서술되어있는 모든 프로퍼티가 담겨져있는것을 내보낸다.
export default counterSlice.reducer;

추가로 Redux는 RTK Query를 통해 서버 상태를 편하게 관리할 수 있고, Redux-thunk 또는 redux-sage를 사용하여 비동기 작업 관리를 관리 할 수 있습니다.

6.2 : Zustand

Redux는 사용하기 편하다고 할 수는 없는것 같습니다. 굳이 소규모 프로젝트를 할때 복잡하게 사용할 필요가 있을까요? 그럴때 사용해 볼 수 있는것이 Zustand 입니다.
Zustand는 경량 상태 관리 라이브러리로, React에서 간단하고 직관적인 상태관리를 가능하게 합니다. 특징은 아래와 같습니다.

  • 간단한 사용법 : 상태 정의와 업데이트 로직을 한 곳에서 관리하며 매우 직관적
  • 작고 가벼운 라이브러리 : 크키가 매우 작고 추가적인 미들웨어나 설정 없이도 대부분의 상태 관리를 가능하게 합니다.
  • 타입 스크립트 지원 : 상태 및 액션을 타입으로 정의하여 타입 안정성을 보장합니다.

아래는 예시입니다.
Zustand는 리덕스와 달리 프로바이더로 감쌀 필요도 없고, 파일 단 한개로 상태와 로직을 구성 할 수 있어서 편리합니다.

App.js ( 전역 상태를 사용하는곳 )

import React from 'react';
import useCounterStore from './redux/store/useCounterStore';
const App = () => {
  const { counterState, increment,decrement,increase,decrease,showCounter,toggleCounter } = useCounterStore();

  return (
    <div style={{ textAlign: 'center', marginTop: '50px' }}>
      {showCounter ? <h1>Counter: {counterState}</h1> : null}
      <button onClick={increment}>1더하기</button>
      <button onClick={decrement}>1빼기</button>
      <button onClick={() => increase(10)}>10더하기</button>
      <button onClick={() => decrease(10)}>10빼기</button>
      <button onClick={toggleCounter}>토글버튼</button>
    </div>
  );
};

export default App;

useCounterStore.js (스토어 파일 상태와 로직이 한 파일로!)


import { create } from "zustand";

const useCounterStore = create((set) => ({
  counterState: 0,
  showCounter: true,

  increment: () =>
    set((state) => ({
      counterState: state.counterState + 1,
    })),
  decrement: () =>
    set((state) => ({
      counterState: state.counterState - 1,
    })),
  increase: (amount) =>
    set((state) => ({
      counterState: state.counterState + amount,
    })),
  decrease: (amount) =>
    set((state) => ({
      counterState: state.counterState - amount,
    })),
    toggleCounter : () => 
        set((state) => ({
            showCounter: !state.showCounter,
        }) )
}));


export default useCounterStore;

너무 신세계잖아......

추가로 Zustand는 서버 상태를 React Query를 결합하여 많이 사용하고, 비동기 처리는 set함수로 처리할수있어서 미들웨어가 따로 필요하지 않습니다.

서버 상태 : 서버에서 제공하는 데이터를 클라이언트 애플리케이션에서 가져와 사용하는 상태

  • 항상 서버와 동기화가 필요합니다.
  • 네트워크 요청(API)를 통해 데이터를 가져옵니다.

비동기 처리 : 시간이 걸리는 작업(네트워크요청,파일 읽기/쓰기)등을 비동기로 처리하여 클라이언트가 멈추지 않게 합니다.

서버 상태는 API 호출과 같은 비동기 작업을 통해 데이터를 가져와 관리하는 식으로 관계가 이루어집니다.

6.3 : React Query

React Query는 서버 상태를 효율적으로 관리하기 위한 라이브러리 입니다.
서버에서 데이터를 가져오거나, 캐싱, 갱신, 에러 처리등을 간단하게 자동화된 방식으로 처리합니다.
클라이언트 상태와 서버 상태를 분리 시킴으로써 효율적인 상태 관리가 가능하게 합니다.

  • 클라이언트 상태와 다르게 서버상태는 비동기 작업, 캐싱, 동기화가 필요하므로 서버 상태의 복잡성을 해결합니다.
  • 최소한의 코드만 작성하여 직관적인 API가 가능해집니다.

Zustand는 클라이언트의 상태를 관리하고, 서버 상태는 React Query로 관리하여 책임을 분리하고 효율적으로 관리합니다.

아래는 React Query의 예시입니다.

index.js (리액트 쿼리는 프로바이더를 제공해야합니다.)

import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));

const queryClient = new QueryClient();


root.render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);

App.js ( 리액트 쿼리 사용해보기 )

import React from 'react';
import useCounterStore from './redux/store/useCounterStore';
import { useQuery } from '@tanstack/react-query';

// API 호출 함수 정의
const fetchData = async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts');
  if (!response.ok) {
    throw new Error('Failed to fetch data');
  }
  return response.json();
};

const App = () => {
  const { counterState, increment, decrement, increase, decrease, showCounter, toggleCounter } =
    useCounterStore();

  // React Query 사용
  const { data, isLoading, isError, error } = useQuery({
    queryKey: ['posts'], // 쿼리 키
    queryFn: fetchData,  // 데이터 가져오기 함수
  });
  return (
    <div style={{ textAlign: 'center', marginTop: '50px' }}>
      {showCounter ? <h1>Counter: {counterState}</h1> : null}
      <button onClick={increment}>1더하기</button>
      <button onClick={decrement}>1빼기</button>
      <button onClick={() => increase(10)}>10더하기</button>
      <button onClick={() => decrease(10)}>10빼기</button>
      <button onClick={toggleCounter}>토글버튼</button>

      {/* 서버 데이터 출력 */}
      <div style={{ marginTop: '20px' }}>
        <h2>Posts</h2>
        {isLoading && <p>Loading...</p>}
        {isError && <p>Error: {error.message}</p>}
        {data && (
          <ul>
            {data.slice(0, 10).map((post) => (
              <li key={post.id}>{post.title}</li>
            ))}
          </ul>
        )}
      </div>
    </div>
  );
};

export default App;

profile
Developer

0개의 댓글