React State Management Libraries

younghyun·2022년 7월 26일
0

(전역) 상태 관리 필요성

React가 Vue나 Angular와 비교했을 때 가장 큰 차이점은 단방향 데이터 바인딩.
React는 단방향 데이터 바인딩을 지원하기 때문에 부모에서 자식으로만 state를 props로 전달할 수 있고, 자식 props를 부모에게 직접 전달할 수는 없음.

자식에서 부모의 상태를 바꾸려면 해당 상태를 컨트롤하는 함수를 props로 넘겨주어야 함.

반복되면 props drilling이 발생함.

props drilling

props가 하위 컴포넌트로 전달하는 용도로만 쓰이는 컴포넌트들을 거치면서 React Component 트리 한 부분에서 다른 부분으로 data를 전달하는 과정.
프로젝트 규모가 커질수록 props 전달이 10, 15개 같이 더 많은 과정을 거치면서, props depth가 증가하고, 불필요한 Re-Rendering 유발됨. props 추적 및 유지보수가 어려워짐.

state/props로 모든 상태를 관리하는 것은 별로인가
컴포넌트 재 활용성이라든가, 의존성 분리 등의 측면에서 props는 잘 다루면 좋은 도구가 될 수 있음. 다만 컴포넌트 역할에 치우쳐진 데이터가 아닌, 프로젝트 전반적으로 사용되는 데이터의 경우 글로벌로 두고 공유하되, 그에 대한 부가적인 데이터들을 state/props로 처리하면 더 효율적.

바인딩(Binding)

프로그램 어떤 기본 단위가 가질 수 있는 구성요소 구체적인 값, 성격을 확정하는 것

int num = 10;

int 자료형 바인딩
num 변수명 바인딩
10 변수값 바인딩

위와 같이 자료형, 변수명, 변수값에 각각 int, num, 10 이라는 구체적인 값을 할당하는 각각의 과정을 바인딩이라고 한다.

실행 이전에 값이 확정되면 정적 바인딩(static binding)이라고 하고,
실행 이후에 값이 확정되면 동적 바인딩(dynamic binding)이라고 한다.

자료형으로 int가 바인딩 되는 것은
프로그램을 컴파일 할 때, 메모리에 할당되므로 정적 바인딩이라고한다.
num 변수명 또한 컴파일 할 때, 메모리에 할당되므로 정적 바인딩이라고한다.
그런데 값 10은 실행 시에 할당되므로 동적 바인딩이라고 한다.

데이터 바인딩(Data Binding)

두 데이터 혹은 정보의 소스를 일치시키는 기법으로, 화면 상에 보여지는 데이터(View)와 브라우저 메모리에 있는 데이터(Model)를 묶어서(Binding) 서로 간 데이터를 동기화(동일하게)하는 것을 의미합니다. 만약 js에서 데이터가 변경되어도 html에서 변경되지 않는다면 사용자는 잘못된 데이터를 보게 됩니다.

데이터 바인딩은 이러한 문제를 해결해줍니다.

FrameWork, Library 와 데이터 바인딩

vanila javascript로 예를 들어보겠습니다.

<div id="view">Hello</div>

 const data = "song";
 function changeText(){
	const view = document.querySelector("#view");
	view.innerText = data;
}

js에 있는 데이터를 화면상에 보여주기 위해서는 아래의 과정들이 있습니다.

  1. document 선택자로 DOM 요소에 접근합니다.
  2. innerText로 텍스트를 변경합니다.

위 코드는 간단하지만 프로젝트가 커질수록 모든 데이터를 제어하기에는 무리가 있습니다.
Angular, Vue, React와 같은 프레임워크/라이브러리들을 사용하면 데이터 바인딩을 보다 깔끔하게 개발할 수 있습니다.

[ 간단한 예제 - 1 ] 값을 데이터 바인딩한다.
💡 아래의 예제는 함수형 컴포넌트 안에서 ‘initData’로 초기화 한 값을 최초 HTML상에 출력합니다. 
이후 ‘재 데이터 바인딩’ 버튼을 누르면 상태 값을 변경시키고, HTML에서 데이터 바인딩을 하는 예시입니다.

import React, { useState } from 'react';
import { CommonType } from '../../type/common/CommonType';

const DataBindingBasicComponent = () => {

    /**
     * 브라우져 메모리상에 존재하는 데이터 
     */
    const [initData, setInitData] = useState({
        greet: "안녕하세요",
        info: "데이터 바인딩에 대해서 공부하고 있습니다."
    });
    
    /**
     * 버튼을 클릭하면 상태로 지정한 메모리로 데이터를 바인딩 한다.
     */
    const reDataBinding = () => {
        setInitData({
            greet: "다시 한번 반갑습니다",
            info: "데이터를 바인딩 합니당"
        })
    };
    return (
        <div>
            <h1>{initData.greet}</h1>
            <h2>{initData.info}</h2>
            <button onClick={reDataBinding}>재 데이터 바인딩</button>
        </div>
    );
}

export default DataBindingBasicComponent;
[ 간단한 예제 - 2 ] CSS 값(스타일)을 데이터 바인딩한다.
💡 아래의 예제는 함수형 컴포넌트 안에서 ‘initCSSStyle’로 초기화 한 ‘스타일’을 최초 HTML상에 출력합니다. 
이후 ‘재 데이터 바인딩’ 버튼을 누르면 상태 값을 변경시키고, HTML에서 데이터 바인딩을 하는 예시입니다.

import React, { useState } from 'react';
const DataBindingCSSComponent = () => {

    /**
     * 브라우져 메모리상에 존재하는 데이터(CSS Style) 
     */
    const [initCSSStyle, setInitCSSStyle] = useState({
        color: "red",
        fontSize: "30"
    });

    /**
     * 버튼을 클릭하면 상태로 지정한 메모리로 데이터를 바인딩 한다.
     */
    const reDataBinding = () => {
        setInitCSSStyle({
            color: "yellow",
            fontSize: "50"
        });
    };
    return (
        <div>
            <h1 style={initCSSStyle}>안녕하세요</h1>
            <h2 style={initCSSStyle}>만나서 반갑습니다.</h2>
            <button onClick={reDataBinding}>재 데이터 바인딩</button>
        </div>
    )
}
export default DataBindingCSSComponent;

단방향 데이터 바인딩은 데이터와 템플릿을 결합하여 화면을 생성합니다.
양방향 데이터 바인딩은 데이터의 변화를 감지해 템플릿과 결합하여 화면을 갱신하고 화면에서의 입력에 따라 데이터를 갱신합니다.(단방향 데이터 바인딩은 이벤트를 통해 데이터를 갱신합니다.) 즉, 데이터와 화면 사이의 데이터가 계속해서 일치되는 것입니다.

양방향 데이터 바인딩은 사용자 UI의 데이터 변경을 감시하는 Watcher와 자바스크립트 데이터의 변경을 감시하는 Watcher가 UI와 자바스크립트 데이터를 자동으로 동기화 시켜주는 방식을 말합니다.

단방향 데이터 바인딩은 단 하나의 Watcher가 자바스크립트의 데이터 갱신을 감지하여 사용자의 UI 데이터를 갱신한다.
사용자가 UI를 통해 자바스크립트의 데이터를 갱신할 때는, 이벤트를 통해 갱신하게 된다.
-> 예를 들어, 리액트에서 Input을 변경할 때 onChange 이벤트를 감지하여 상태를 변경시킨다. 그리고, Watcher가 이 상태를 감지하여 UI를 업데이트를 하게 된다. 이 방식이 바로 단방향 데이터 바인딩이라고 볼 수 있다.

단방향 데이터 바인딩

💡 컴포넌트 내에서 '단방향 데이터 바인딩'은 Javascript(Model)에서 HTML(View -> 사용자 화면)로 한 방향으로만 데이터를 동기화(화면 변경됨)하는 것을 의미합니다. [JS(Model) -> HTML(View)]

💡 단방향 데이터 바인딩이기에 역으로 HTML(View)에서 JS(Model)로의 직접적인 데이터 갱신은 불가능합니다. '이벤트 함수(onClick, onChange,...)'를 주고 함수를 호출한 뒤 Javascript에서 HTML로 데이터를 변경해야 합니다.(변화된 값을 감지해 바인딩을 시켜주어야 합니다.) [HTML(View) -> JS(Model)]

💡 컴포넌트 간에서 단방향 데이터 바인딩은 부모 컴포넌트에서 자식 컴포넌트로만 데이터가 전달되는 구조입니다.

대표적으로 React가 단방향 데이터 바인딩을 한다.

장점

  • 데이터 변화에 따른 성능 저하 없이 DOM 객체 갱신 가능.
  • 데이터 흐름이 단방향(부모->하위 컴포넌트)이라, 코드를 이해하기 쉽고 데이터 추적과 디버깅이 쉬움.

단점

  • 사용자 입력에 따라 데이터를 갱신하고 화면을 업데이트 해야 하므로 단방향 데이터 바인딩으로 구성하면, 데이터 변화를 감지하고 화면을 업데이트 하는 코드를 매번 작성해주어야함.
React

const [inputValue, setInputValue] = useState("");

<input value={inputValue} onChange={e => setInputValue(e.target.value)} />

react를 모르시는 분들을 위하여 useState를 살짝 설명드리면 앞에 나오는 부분을 [get, set]이라고 생각하시면 됩니다.

inputValue는 변수를 활용할 때 쓰이고 setInputValue는 inputValue의 값을 바꿀 때 사용합니다.

위 코드의 흐름은 이렇습니다.

1. 초기값이 ""인 inputValue를 input의 value값으로 넣습니다.

2. 사용자가 값을 변경할 때 onChange가 감지합니다.

3. 변경된 값을 setInputValue로 inputValue의 값을 업데이트 시켜줍니다.

4. 변경된 값을 value를 통해 사용자에게 보여줍니다.

이렇듯 사용자가 변수를 직접적으로 변경하는 것이 아닌 저희가 변경을 감지하여 변수의 값을 바꿔줍니다.

양방향 데이터 바인딩

💡 컴포넌트 내에서 '양방향 데이터 바인딩'은 Javascript(Model)와 HTML(View) 사이에 ViewModel이 존재하여 하나로 묶여서(Binding) 되어서 둘 중 하나만 변경되어도 함께 변경되는 것을 의미합니다. [HTML(View) <-> ViewModel <-> Javascript(Model)](JS데이터의 변화를 감지해 템플릿과 결합해 화면을 갱신(변경), 화면(사용자) 입력에 따라 JS데이터를 갱신(변경)하는 것이다.

데이터 변경을 프레임워크에서 감지하고 있다가, 데이터가 변경되는 시점에 DOM 객체에 렌더링을 해주거나 페이지 내에서 모델의 변경을 감지해 JS 실행부에서 변경한다. 입력된 값이나 변경된 값에 따라 내용이 바로 바뀌기 때문에 따로 체크해주지 않아도 된다.

컨트롤러와 뷰 양쪽 데이터 일치가 모두 가능한 것.
(HTML -> JS, JS -> HTML 양쪽 모두 가능.)

💡 컴포넌트 간에서는 부모 컴포넌트에서 자식 컴포넌트로는 Props를 통해 데이터를 전달하고, 자식 컴포넌트에서 부모 컴포넌트로는 Emit Event를 통해서 데이터를 전달하는 구조입니다.

대표적으로 SPA Framework에서는 Vue.js, Angular에서 양방향 데이터 바인딩을 합니다.

장점 : 양방향 데이터 바인딩은 웹 애플리케이션의 복잡도가 증가하면 증가할수록 빛을 발한다. 수많은 코드의 양을 줄여줄 뿐만 아니라 유지보수나 코드를 관리하기 매우 쉽게 해주기 때문이다.(데이터 자동 변경)
단점 : 변화에 따라 DOM 객체 전체를 렌더링(바인딩 된 모든 데이터(요소, 상태)에 관해 일괄적으로 DOM을 렌더링하기 때문)해주거나 데이터를 바꿔주므로, 성능이 감소되는 경우가 있음.

예를 들어 vue.js 에서는
V-model과 V-on을 통해 양방향 데이터 바인딩을 한다.
v-model이 DOM 연관된 내용을 잡아내고, vue가 바라보는 대상의 속성과 연결됨.
v-on은 이벤트를 잡아내는 데 사용한다.

데이터 바인딩 - 변화 감지
사실 모델이 변화할 가능성이 있는 경우는 그다지 많지 않다.
아래와 같은 비동기 처리가 수행될 때 컴포넌트 클래스의 데이터가 변경될 수 있다. 변화 감지는 모델이 변화할 수 있는 이러한 상황들을 감시한다.

DOM 이벤트(click, key press, mouse move 등)
Timer 함수(setTimeout, setInterval)의 tick 이벤트
Ajax 통신 / Promise

SoftWare Arichitecture

소프트웨어를 구성하는 구성요소(모듈 / 컴포넌트 등) 간의 관계를 관리하는 시스템의 구조.
소프트웨어의 설계와 업그레이드를 통제하는 지침과 원칙

기본원리

1. 모듈화
시스템의 기능들을 모듈 단위로 나누는 것. 이때 모듈 개수는 적당히. 너무 많을 경우 통합 비용이, 너무 적을 경우 모듈 하나의 개발 비용이 많이 들게 됨.

2. 추상화
전체적이고 포괄적인 개념 설계 -> 차례로 세분화 / 구체화

3. 단계적 분해
문제를 상위의 중요 개념으로부터 하위의 개념으로 구체화시키며 분할

4. 정보 은닉
한 모듈 내부에 포함된 절차와 자료를 숨기며 독립적 수행을 가능캐 해, 다른 모듈이 접근하거나 변경하지 못하게 하기

:: 모듈 내 응집도는 강하게, 모듈 간 결합도는 약하게가 포인트.

Software Design Pattern

소프트웨어 설계 시 특정 맥락에서 자주 발생하는 / 공통적인 문제들을 해결하기 위한 방법 중 하나로서 과거의 개발 과정에서 발견된 설계의 노하우를 축적, 재사용하기 좋은 좋은 형태로 묶어 정리한 일종의 설계 디자인 방법론.

Architecture Pattern(Architecture Style)

주어진 상황의 소프트웨어 아키텍처에서 발생하는 공통 문제에 대해 이를 해결하기 위한 일반화, 재사용 가능한 해결책
아키텍처 설계 시 이를 위한 기본적 윤곽을 제시. 서브 시스템들과의 역할 정의, 관계 간 여러 규칙 등이 포함 되어있음.

MVC Pattern

Model, View, Controller의 약자. 사용자 인터페이스, 데이터 및 논리 제어를 구현하는데 널리 사용되는 소프트웨어 디자인 패턴입니다. 소프트웨어의 비즈니스 로직과 화면을 구분 하는데 중점을 두고있습니다.
Model 에 데이터를 정의해 놓고, Controller 를 통해서 Model 의 데이터를 CRUD 작업하며, 변경된 데이터를 View 에 출력하는 식의 패턴입니다.

EX)

Model
모델은 앱이 포함해야할 데이터가 무엇인지를 정의합니다. 데이터의 상태가 변경되면 모델은 일반적으로 뷰에게 알리며(따라서 필요한대로 화면을 변경할 수 있습니다) 가끔 컨트롤러에게 알리기도 합니다(업데이트된 뷰를 제거하기 위해 다른 로직이 필요한 경우).

다시 쇼핑 리스트 앱으로 돌아가서, 모델은 리스트 항목이 포함해야 하는 데이터 — 품목, 가격, 등. — 와 이미 존재하는 리스트 항목이 무엇인지를 지정합니다.

View
레이아웃, 화면 처리. 뷰는 앱의 데이터를 보여주는 방식을 정의합니다. View는 사용자로부터 데이터를 입력받기도 하기 때문에 사용자의 입력이 Model에 영향을 주기도 합니다.

쇼핑 리스트 앱에서, 뷰는 항목이 사용자에게 보여지는 방식을 정의하며, 표시할 데이터를 모델로부터 받습니다.

Controller
명령을 Model, View로 routing. 앱의 사용자로부터의 입력에 대한 응답으로 모델 및/또는 뷰를 업데이트하는 로직을 포함합니다.

예를 들어보면, 쇼핑 리스트는 항목을 추가하거나 제거할 수 있게 해주는 입력 폼과 버튼을 갖습니다. 이러한 액션들은 모델이 업데이트되는 것이므로 입력이 컨트롤러에게 전송되고, 모델을 적당하게 처리한다음, 업데이트된 데이터를 뷰로 전송합니다.

단순히 데이터를 다른 형태로 나타내기 위해 뷰를 업데이트하고 싶을 수도 있습니다. (예를 들면, 항목을 알파벳순서로 정렬한다거나, 가격이 낮은 순서 또는 높은 순서로 정렬). 이런 경우에 컨트롤러는 모델을 업데이트할 필요 없이 바로 처리할 수 있습니다.

문제점
MVC Pattern 근본적인 문제에 대해 설명하자면 사용자와 상호작용이 View에서 일어났기 때문에 사용자의 입력에 따라 Model을 업데이트 해줘야 하는 경우가 있고 여기서 의존성의 이유로 하나의 모델만이 아닌 다른 모델 까지 업데이트 해야 할때도 있었을 것.

그 외에도 가끔 이러한 문제때문에 아주 많은 변경을 초래 하는 경우도 있었음.

(핑퐁 게임을 생각. 하나의 공을 주고 받는 것은 어렵지 않지만 아주 많은 공을 주고 받을때에는 공이 어디로 갈지 모르고 상당히 어려워짐.)

해결 방법
Facebook 개발진들은 이에 대한 해결책으로 단방향 데이터 흐름 택함.
단방향 데이터 흐름을 가지는 구조는 데이터는 단방향으로만 흐르고, 새로운 데이터를 넣으면 처음 부터 다시 시작되는 방식으로 설계됨.
이러한 시스템 구성을 FLUX 구조라 부름.

Flux Pattern


Flux 구조의 가장 큰 특징은 단방향 데이터 흐름.

사용자 입력을 기반으로 Action을 만들고 Action을 Dispatcher에 전달하여 Store(Model)의 데이터를 변경한 뒤 View에 반영하는 단방향의 흐름으로 애플리케이션을 만드는 아키텍처입니다.

Action
Action이란 데이터를 변경하는 행위로서 Dispatcher에게 전달되는 객체를 말합니다.
Dispatcher에서 콜백 함수가 실행 되면 Store가 업데이트 되게 되는데, 이 콜백 함수를 실행 할 때 데이터가 담겨 있는 객체(Action)가 인수로 전달되어야 합니다.
Action은 대채로 액션 생성자(Action creator)에서 만들어집니다.
Action creator 메서드는 새로 발생한 Action의 타입(type)과 새로운 데이터(payload)를 묶어 Dispatcher에게 전달합니다.

예를 들어 GET_POST라는 게시글을 가져와서 스토어 상태값을 변경해 주는 함수를 실행하고 싶을 때는 GET_POST라는 이름의 액션을 발생 시키는 것.
 
dispatch({ <= 디스패쳐
	type: GET_POST, <= 액션 이름
    // GET_POST을 통해 스토어에 변화가 일어남
})
여기에 payload 값을 넣어 데이터를 관리할 수 있음.
{
  type: 'SET_PROFILE',
  data: {
    'name': 'Harry',
    'age': 458
  }
}

Dispatcher
Dispatcher는 Flux 어플리케이션의 모든 데이터 흐름을 관리하는 일종의 허브 역할.

switch 문으로 들어오는 Action 객체를 나누어 처리합니다. Action을 감지하면 Action 객체의 type 을 구분해 Dispatcher에 Store가 등록해놓은 콜백 함수를 실행합니다.
( Store에 접근하기 위한 일종의 단계이고 Action을 통해 Store에 접근하기 위해서는 Dispatcher 단계를 거쳐야 합니다.)

Store에 있는 상태 사이의 의존성이 있을 경우 이를 관리하는 것도 Dispatcher가 처리합니다.

Store
Store는 상태 저장소로서 상태와 상태를 변경할 수 있는 메서드를 가지고 있습니다.
( 모든 상태 변경은 Store에 의해 결정되며 상태 변경을 위한 요청을 Store에 직접 할 수는 없음. )
어떤 type의 Action이 발생했는지에 따라 그에 맞는 데이터 변경을 수행하는 콜백 함수를 Dispatcher에 등록합니다.
Dispatcher에서 콜백 함수를 실행하여 상태가 변경되면 View에게 데이터가 변경되었음을 알립니다.

View or Contoller View
View는 상태를 가져와서 보여주고 사용자로 부터 입력 받을 화면을 보여줍니다.
Contoller View는 Store와 View의 중간 관리자 같은 역할을 하고 Store에서 상태 값 변경이 일어났을 때 Store는 그 사실을 Contoller View에 전달하고 Contoller View는 자신 아래에 모든 View에게 새로운 상태를 넘겨줌.

Redux

JavaScript State Management Library

Redux가 나오기 이전, React를 포함한 대부분 프로젝트는 MVC 아키텍처가 많이 사용. 컨트롤러가 여러 모델을 제어하고, 모델과 뷰가 서로 바라보는 구조로, 모델과 뷰가 양방향으로 영향을 미치기 때문에 프로젝트의 규모가 커지고 상태가 많아질수록 관리가 어려워짐.

한 번의 변경으로 수많은 상태 변경 과정이 이뤄져야 하며, 이 과정에서 불필요한 렌더링과 state 처리가 발생. 어디서 오류, 변경이 발생했는지, 어디까지 변화가 일어나는지 추적하기 어려움.

그렇게 페이스북이 내놓은 새로운 아키텍처가 Flux 아키텍처. 데이터의 흐름이 단방형으로 흐르는 구조




이러한 Flux 아키텍처를 가져가는 라이브러리로 대표적인 것이 Redux.
Redux와 같은 Flux 아키텍처의 라이브러리의 경우, store에 모든 상태를 저장하는 중앙집중방식.

Redux를 쓰면, 상태 관리를 Component 바깥에서 한다

상태값을, 컴포넌트에 종속시키지 않고, 상태 관리를 컴포넌트의 바깥에서 관리 할 수 있게 됩니다.

B 에서 일어나는 변화가 G 에 반영된다고 가정

리덕스를 프로젝트에 적용하게 되면 이렇게 Store 라는 녀석이 생깁니다.
Store 안에는 프로젝트의 상태에 관한 데이터들이 담겨있습니다.

G 컴포넌트는 스토어에 구독을 합니다. 구독을 하는 과정에서, 특정 함수가 스토어한테 전달이 됩니다. 그리고 나중에 스토어의 상태값에 변동이 생긴다면 전달 받았던 함수를 호출해줍니다.

이제 B 컴포넌트에서 어떤 이벤트가 생겨서, 상태를 변화 할 일이 생겼습니다. 이 때 dispatch 라는 함수를 통하여 액션을 스토어한테 던져줍니다. 액션은 상태에 변화를 일으킬 때 참조 할 수 있는 객체입니다. 액션 객체는 필수적으로 type 라는 값을 가지고 있어야 합니다.

예를들어 { type: 'INCREMENT' } 이런 객체를 전달 받게 된다면, 리덕스 스토어는 아~ 상태에 값을 더해야 하는구나~ 하고 액션을 참조하게 됩니다.

추가적으로, 상태값에 2를 더해야 한다면, 이러한 액션 객체를 만들게 됩니다: { type: 'INCREMENT', diff: 2 }

그러면, 나중에 이 diff 값을 참고해서 기존 값에 2를 더하게되겠죠. type 를 제외한 값은 선택적(optional) 인 값입니다.

액션 객체를 받으면 전달받은 액션의 타입에 따라 어떻게 상태를 업데이트 해야 할지 정의를 해줘야합니다. 이러한 업데이트 로직을 정의하는 함수를 리듀서라고 부릅니다. 이 함수는 직접 구현합니다. 예를들어 type 이 INCREMENT 라는 액션이 들어오면 숫자를 더해주고, DECREMENT 라는 액션이 들어오면 숫자를 감소시키는 그런 작업을 여기서 하면 됩니다.

리듀서 함수는 두가지의 파라미터를 받습니다.

state: 현재 상태
action: 액션 객체
그리고, 이 두가지 파라미터를 참조하여, 새로운 상태 객체를 만들어서 이를 반환합니다.

상태에 변화가 생기면, 이전에 컴포넌트가 스토어한테 구독 할 때 전달해줬었던 함수 listener 가 호출됩니다. 이를 통하여 컴포넌트는 새로운 상태를 받게되고, 이에 따라 컴포넌트는 리렌더링을 하게 됩니다.

정리
기존에는 부모에서 자식의 자식의 자식까지 상태가 흘렀었는데, 리덕스를 사용하면 스토어를 사용하여 상태를 컴포넌트 구조의 바깥에 두고, 스토어를 중간자로 두고 상태를 업데이트 하거나, 새로운 상태를 전달받습니다.
따라서, 여러 컴포넌트를 거쳐서 받아올 필요 없이 아무리 깊숙한 컴포넌트에 있다 하더라도 직속 부모에게서 받아오는 것 처럼 원하는 상태값을 골라서 props 를 편리하게 받아올 수 있습니다.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>그냥 평범한 리덕스</title>
</head>
<body>
  <h1 id="number">0</h1>
  <button id="increment">+</button>
  <button id="decrement">-</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.6.0/redux.js"></script>
</body>
</html>

// 편의를 위하여 각 DOM 엘리먼트에 대한 레퍼런스를 만들어줍니다.
const elNumber = document.getElementById('number');
const btnIncrement = document.getElementById('increment');
const btnDecrement = document.getElementById('decrement');

// 액션 타입을 정의해줍니다. 
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// 액션 객체를 만들어주는 액션 생성 함수
const increment = (diff) => ({ type: INCREMENT, diff: diff });
const decrement = () => ({ type: DECREMENT });

// 초기값을 설정합니다. 상태의 형태는 개발자 마음대로 입니다.
const initialState = {
  number: 0
};

/* 
   이것은 리듀서 함수입니다.
   state 와 action 을 파라미터로 받아옵니다.
   그리고 그에 따라 다음 상태를 정의 한 다음에 반환해줍니다.
*/

// 여기에 state = initialState 는, 파라미터의 기본값을 지정해줍니다.
const counter = (state = initialState, action) => {
  console.log(action);
  switch(action.type) {
    case INCREMENT:
      return { 
        number: state.number + action.diff
      };
    case DECREMENT:
      return { 
        number: state.number - 1
      };
    default:
      return state;
  }
}

// 스토어를 만들 땐 createStore 에 리듀서 함수를 넣어서 호출합니다.
const { createStore } = Redux;
const store = createStore(counter);


// 상태가 변경 될 때 마다 호출시킬 listener 함수입니다
const render = () => {
  elNumber.innerText = store.getState().number;
  console.log('내가 실행됨');
}

// 스토어에 구독을하고, 뭔가 변화가 있다면, render 함수를 실행합니다.
store.subscribe(render);

// 초기렌더링을 위하여 직접 실행시켜줍니다.
render();


// 버튼에 이벤트를 달아줍니다.
// 스토어에 변화를 일으키라고 할 때에는 dispatch 함수에 액션 객체를 넣어서 호출합니다.

btnIncrement.addEventListener('click', () => {
  store.dispatch(increment(25));
})


btnDecrement.addEventListener('click', () => {
  store.dispatch(decrement());
})
  1. 액션타입을 만들어주었습니다.
  2. 그리고 각 액션타입들을 위한 액션 생성 함수를 만들었습니다. 액션 함수를 만드는 이유는 그때 그때 액션을 만들 때마다 직접 { 이러한 객체 } 형식으로 객체를 일일히 생성하는 것이 번거롭기 때문에 이를 함수화 한 것입니다. 나중에는 특히, 액션에 다양한 파라미터가 필요해 질 때 유용합니다.
  3. 변화를 일으켜주는 함수, 리듀서를 정의해주었습니다. 이 함수에서는 각 액션타입마다, 액션이 들어오면 어떠한 변화를 일으킬지 정의합니다. 지금의 경우에는 상태 객체에 number 라는 값이 들어져있습니다. 변화를 일으킬 때에는 불변성을 유지시켜주어야 합니다.
  4. 스토어를 만들었습니다. 스토어를 만들 땐 createStore 를 사용하며 만듭니다. createStore 에는 리듀서가 들어갑니다. (스토어의 초기상태, 그리고 미들웨어도 넣을 수 있습니다.)
  5. 스토어에 변화가 생길 때 마다 실행시킬 리스너 함수 render 를 만들어주고, store.subscribe 를 통하여 등록해주었습니다.
  6. 각 버튼의 클릭이벤트에, store.dispatch 를 사용하여 액션을 넣어주었습니다.

3가지 규칙

  1. Single source of truth(하나의 애플리케이션 안에는 하나의 스토어가 있습니다.)
    하나의 애플리케이션에선 단 한개의 스토어를 만들어서 사용합니다.
    여러개의 스토어를 만들고 싶다면 만들 수는 있습니다. 특정 업데이트가 너무 빈번하게 일어나거나, 애플리케이션의 특정 부분을 완전히 분리시키게 될 때 그렇게 여러개의 스토어를 만들 수도 있습니다. 하지만 그렇게 하면, 개발 도구를 활용하지 못하게 됩니다.

  2. State is read-only(상태는 읽기전용 입니다.)
    React에서 state 를 업데이트 해야 할 때, setState 를 사용하고, 배열을 업데이트 해야 할 때는 배열 자체에 push 를 직접 하지 않고, concat 같은 함수를 사용하여 기존의 배열은 수정하지 않고 새로운 배열을 만들어서 교체하는 방식으로 업데이트를 합니다. 엄청 깊은 구조로 되어있는 객체를 업데이트를 할 때도 마찬가지로, 기존의 객체는 건들이지 않고 Object.assign 을 사용하거나 spread 연산자 (...) 를 사용하여 업데이트를 합니다.

    리덕스에서도 마찬가지입니다. Action이라는 객체를 통해서만 상태를 변경할 수 있습니다. 기존의 상태는 건들이지 않고 새로운 상태를 생성하여 업데이트 해주는 방식으로 해주면, 나중에 개발자 도구를 통해서 뒤로 돌릴 수도 있고 다시 앞으로 돌릴 수도 있습니다.

    리덕스에서 불변성을 유지해야 하는 이유는 내부적으로 데이터가 변경 되는 것을 감지하기 위하여 shallow equality 검사를 하기 때문입니다.
    객체의 변화를 감지 할 때 객체의 깊숙한 안쪽까지 비교를 하는 것이 아니라 겉핥기 식으로 비교를 하여 좋은 성능을 유지할 수 있는 것입니다.

  3. Changes are made with pure functions(변화를 일으키는 함수, 리듀서는 순수한 함수여야 합니다.)
    변경은 순수함수로만 가능합니다. 리듀서 함수는 이전 상태와, 액션 객체를 파라미터로 받습니다.
    이전의 상태는 절대로 건들이지 않고, 변화를 일으킨 새로운 상태 객체를 만들어서 반환합니다.
    똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과값을 반환해야만 합니다.

구성

  1. Store
    상태가 관리되는 오직 하나의 공간. 컴포넌트와는 별개로 스토어라는 공간이 있어서 그 스토어 안에 앱에서 필요한 상태를 담습니다.
    컴포넌트에서 상태 정보가 필요할 때 스토어에 접근한다.
    스토어 안에는, 현재의 앱 상태와, 리듀서가 들어가있고, 추가적으로 몇가지 내장 함수들이 있습니다.
  • 디스패치(dispatch)
    스토어의 내장함수 중 하나입니다. 디스패치는 액션을 발생 시키는 것 입니다. action을 파라미터로 전달하고 reducer를 호출합니다.
    그렇게 호출을 하면, 스토어는 리듀서 함수를 실행시켜서 해당 액션을 처리하는 로직이 있다면 액션을 참고하여 새로운 상태를 만들어줍니다.
    EX) dispatch(action)
  • 구독(subscribe)
    구독 또한 스토어 내장함수 중 하나 입니다. 함수 형태의 값을 파라미터로 받아옵니다. subscribe 함수에 특정 함수를 전달해주면 action이 dispatch 되었을 때마다 전달된 함수가 호출됩니다.

    리액트에서 리덕스를 사용하게 될 때 보통 이 함수를 직접 사용하는 일은 별로 없습니다. 그 대신에 react-redux 라는 라이브러리에서 제공하는 connect 함수 또는 useSelector Hook 을 사용하여 리덕스 스토어의 상태에 구독합니다.
  1. Action (액션)
    앱에서 스토어에 운반할 데이터(상태를 변화시키려는 의도를 표현한 객체)를 말합니다. 상태에 어떠한 변화가 필요하게 될 때, 액션을 발생시킵니다.
    자바스크립트 객체 형식으로 되어있습니다. type 필드를 필수적으로 가지고 있어야하고 그 외의 값들은 개발자 마음대로 넣어줄 수 있습니다.

    action type과 전송할 데이터(payload)로 이루어져 있습니다.
const ADDTODO = 'todo/ADDTODO';
const DELETETODO = 'todo/DELETETODO';
-> 타입은 다른 모듈과 헷갈릴 수 있으니 앞에 모듈명을 적어주는 게 좋음.

{
  type: 'ACTION_CHANGE_USER', // 필수
  payload: { // 옵션
    name: '하나몬',
    age: 100
  }
}

{
  type: "ADD_TODO",
  data: {
    id: 0,
    text: "리덕스 배우기"
  }
}

{
  type: "CHANGE_INPUT",
  text: "안녕하세요"
}
  1. 액션 생성 함수 (Action Creator)
    Action이 동작에 대해 선언된 객체라면, Action Creator는 Action을 생성해 실제 객체로 만들어 주는 함수입니다.
    파라미터를 받아와서 액션 객체 형태로 만듭니다.
export function addTodo(data) {
  return {
    type: "ADD_TODO",
    data
  };
}

// 화살표 함수로도 만들 수 있습니다.
export const changeInput = text => ({ 
  type: "CHANGE_INPUT",
  text
});

이러한 액션 생성함수를 만들어서 사용하는 이유는 나중에 컴포넌트에서 더욱 쉽게 액션을 발생시키기 위함입니다. 그래서 보통 함수 앞에 export 키워드를 붙여서 다른 파일에서 불러와서 사용합니다.

리덕스를 사용 할 때 액션 생성함수를 사용하는것이 필수적이진 않습니다. 액션을 발생 시킬 때마다 직접 액션 객체를 작성할수도 있습니다.

  1. Reducer (리듀서)
    Action을 통해 그 결과 어플리케이션의 상태가 어떻게 바뀌는지 특정하는 즉, state에 변화를 일으키는 함수입니다.
    reducer는 현재(이전)의 state 와 action 을 인자로 받아 store에 접근해 action 에 맞춰 state를 변경합니다. 앞서 언급한 reducer는 순수 함수임을 지켜야 하는 원칙으로, 이전의 상태는 건드리지 않고 변화된 새로운 상태 객체를 만들어 반환합니다.
function reducer(state, action) {
  // 상태 업데이트 로직
  return alteredState;
}

리듀서는, 현재의 상태와, 전달 받은 액션을 참고하여 새로운 상태를 만들어서 반환합니다. 이 리듀서는 useReducer 를 사용할때 작성하는 리듀서와 똑같은 형태를 가지고 있습니다.

만약 카운터를 위한 리듀서를 작성한다면 다음과 같이 작성할 수 있습니다.

function counter(state, action) {
  switch (action.type) {
    case 'INCREASE':
      return state + 1;
    case 'DECREASE':
      return state - 1;
    default:
      return state;
  }
}

useReducer 에선 일반적으로 default: 부분에 throw new Error('Unhandled Action')과 같이 에러를 발생시키도록 처리하는게 일반적인 반면 리덕스의 리듀서에서는 기존 state를 그대로 반환하도록 작성해야 합니다.

리덕스를 사용 할 때에는 여러개의 리듀서를 만들고 이를 합쳐서 루트 리듀서 (Root Reducer)를 만들 수 있습니다. (루트 리듀서 안의 작은 리듀서들은 서브 리듀서라고 부릅니다.)

  • Action(액션)을 Reducer(리듀서)에 전달 해야합니다.
  • Reducer(리듀서)가 주문을 보고 Store(스토어)의 상태를 업데이트하는 것입니다.( 새로운 상태를 반환함. )
  • Action(액션)을 Reducer(리듀서)에 전달하기 위해서는 dispatch() 메소드를 사용해야한다.
    (Action(액션) 객체가 dispatch() 메소드에 전달된다. -> dispatch(액션)를 통해 Reducer를 호출한다. -> Reducer는 새로운 Store 를 생성합니다. )

언제 쓰는가(사용 이유)

  • 전역 상태가 필요하다고 느껴질 때
  • 상태들이 자주 업데이트 될 때
  • 상태를 업데이트 하는 로직이 복잡할 때
  • 앱이 크고 많은 사람들에 의해 코드가 관리될 때
  • 상태가 업데이트되는 시점을 관찰할 필요가 있을 때

장점

  • action, reducer, selector, store를 세팅하는 보일러플레이트 코드는 유지보수에 유리
  • reducer와 store의 등장을 통해 복잡한 상태 관리를 효율적이고 예측 가능하게 함. (모든 상태 변화가 store에 집중되어 있고, reducer가 순수함수를 사용하며, 상태 변화가 단방향으로 일어나기 때문)
  • 데이터 관리에 보수적 : 확장 및 디버깅 유리
  • 하나의 store, 하나의 객체 트리를 가지며, redux dev tool이 있어 디버깅이 용이함.
  • 테스트 붙이기 용이함. (reducer가 순수함수 사용하기 때문)
  • 안정적, 이용자가 많음.
  • 비동기를 지원하는 Redux Saga, Redux Thunk 등 다양한 미들웨어가 존재합니다.

단점

  • 데이터 관리에 보수적 : 높은 러닝커브와 큰 보일러플레이트 코드(상태의 개수가 적더라도 보일러플레이트 코드가 크기 때문에 불편함.)
  • store는 외부 요소이기 때문에 리액트의 내부 스케줄러에 접근할 수가 없음.
  • redux가 비동기 데이터 처리를 하기 위해서는 redux-saga와 같은 별도 라이브러리 추가적으로 사용해야 함.

보이러 플레이트 코드
최소한의 변경으로 여러곳에서 재사용되며, 반복적으로 비슷한 형태를 띄는 코드

CRA
CRA는 Create-React-App 라는 리액트 공식 명령어의 약자

npx create-react-app 프로젝트명(서비스명)

참고로 npx는 npm의 CLI 명령어.
명령어를 직역하자면 '리액트 어플리케이션 만들기'
IDE나 에디터로 프로젝트를 생성할 수 있지만 CRA로 생성하면 웹팩(Webpack) 등의 세팅들을 알아서 해줌.
IDE나 에디터로 생성할 시 해당 세팅을 일일이 해줘야 함.

CRA로 생성한 어플리케이션에는 웹팩 설정 파일이 안 보이는데 그건 CRA가 숨겨놓음. package.json의 scripts를 통해 파일 경로를 찾을 수 있다.

그렇다면 왜 사람들은 CRA를 보일러플레이트라고 부르는 걸까?

CRA = Bioler Plate?
CRA에는 '변경 없는 (어플리케이션을 생성해주는) 코드'들이 있고 이 코드들은 '여러 곳(정확히는 여러 사람들에게)'에서 쓰인다.
이는 보일러플레이트의 역할과 일치하므로 CRA는 리액트의 보일러 플레이트

Recoil

리액트를 위한 상태관리 라이브러리.
Version : 0.72
Git : https://github.com/facebookexperimental/Recoil

Frontend 개발을 하면서 state 를 관리하는 방법은 여러가지 방법이 있습니다.

첫 번째로, 라이브러리를 통해 관리 하지 않고 직접 state 를 관리하는 방법입니다. 이 방법은 작은 프로젝트에서 유효할 수 있습니다. 하지만 규모가 있는 애플리케이션에서 직접 state 를 관리하게 된다면 props drilling 이 심각하게 발생하거나, state 가 어디서 관리되고 있는지 개발자도 모르는 등, 커다란 문제에 직면할 수 있습니다.

두 번째는 React 에서 자체적으로 제공하고 있는 Context API 를 사용하는 것입니다. React 팀은 16.3 버전에서 라이브러리를 사용하지 않아도 자체적으로 전역 상태를 관리할 수 있는 API 를 출시 했습니다. 그런데 React 팀에서 공식적으로 출시한 라이브러리가 있음에도 불구하고, 왜 대부분의 팀은 Redux 와 같은 상태 관리 라이브러리를 사용할까요? Context API 를 사용한다면 익혀야 하는 기본적인 개념과 작성해야 하는 코드의 길이가 Redux 에 비해 크게 체감되지 않습니다. 그렇기에 기존의 코드에서 벗어나 Context API 를 사용할 필요가 없는 것입니다.

세 번째는 상태관리 라이브러리의 사용입니다. React 에서 흔히 사용 되는 상태 관리 라이브러리는 react-redux 입니다.
Redux 를 사용하면서 마주치게 된 가장 큰 어려움은 커다란 보일러 플레이트입니다.
mobx 또한 많이 사용되지만,비교적 최근에 나온 Recoil 은 페이스북에서 출시한, React 만을 위한 상태 관리 라이브러리 입니다.

사실 비교하는 게 무의미 할 정도의 격차를 살펴볼 수 있습니다. 라이브러리 간 격차가 왜 이렇게 두드러지게 보일까요?

출시일 Recoil 은 2020년 5월에 출시된 State Management Library 이다. 그에 반해서 Redux 7년 전에 생성되었을 만큼 가장 역사 깊은 라이브러리다.
익숙함 개발을 하는데 있어서 중요한 건 익숙함이다. 익숙함이란 숙련도라고도 이야기 할 수 있을 것 같은데, Redux 가 오래된 만큼 팬층이 깊고 숙련도 깊은 사람들이 많기 때문이다. 즉 요즘 말로 고인물 들이 많다.

Recoil 시작하기

Recoil 을 시작하기 위해선 index.tsx (혹은 index.jsx) 에서 렌더링 하고 있는 root 를 RecilRoot 를 통해서 감싸줘야 합니다. 마치 Redux 에서 <Provider> 를 통해서 App 에 store 을 연결해주는 것과 비슷한 과정입니다. Redux 에서는 하나의 store 를 연결해주는 과정이지만, Recoil 에서는 atom 들이 떠다니는 Root 를 설정해준다고 추상화 하면 좋을 것 같습니다.(애플리케이션에서 Recoil을 연동하기 위해 사용합니다.)

즉, 컴포넌트들이 전역상태를 사용하기 위해선 RecoilRoot 컴포넌트를 부모 컴포넌트로 가져야합니다. RecoilRoot 컴포넌트의 하위 컴포넌트들이 전역상태를 가져다가 쓰는 것

기본적인 작동 원리는 해당 컴포넌트의 가장 가까운 곳에 위치한 RecoilRoot 컴포넌트를 사용합니다.

RecoilRoot 컴포넌트를 위치시킬 가장 좋은 곳은 root 컴포넌트(index.js)에 최상위 컴포넌트로 <RecoilRoot> 를 두는 것입니다.

  • 여러 개 RecoilRoot를 사용 가능하며 이러한 경우에 각각의 RecoilRoot는 독립적인 provider/store로 동작.
  • RecoilRoot override 속성이 false인 경우, atom state는 각각의 RecoilRoot에 따라 다른 값을 갖게 됨.
  • RecoilRoot override 속성이 true인 경우, atom state는 중첩된 RecoilRoot에 따라 동일한 값을 갖게 됨.
import * as ReactDOMClient from "react-dom/client";
import { RecoilRoot } from "recoil";

import App from "./App";

const rootElement = document.getElementById("root");
const root = ReactDOMClient.createRoot(rootElement);

root.render(
  <RecoilRoot>
    <App />
  </RecoilRoot>
)
;

Recoil 핵심개념

1. atom
Recoil 의 첫 번째 핵심 개념은 atom 입니다. 간단히 이해하고자 한다면 atom 을 비눗방울로 추상화 할 수 있습니다. 우리가 만드는 Web Application 을 구조화 한다면 그 구조의 상단에 atom 이 비눗방울 처럼 둥둥 떠다니고 있다고 추상화 할 수 있겠습니다. 만약 개발을 하다가 어떤 비눗방울 (상태) 이 필요하다면 해당하는 비눗방울만 쏙 빼서 쉽게 사용할 수 있습니다.

  • Redux에서 쓰이는 store와 유사한 개념. 상태를 정의하는 방법
  • 상태 정의 시 unique 한 id인 key(전역상태 이름, 전역 상태들 사이에서 고유한 값을 가져야 함.), default(전역 상태 초기 내용)설정
  • 전역적으로 사용하길 원하는 state 를 atom 이라는 비눗방울로 띄어서 어디서든지 사용할 수 있도록 만드는 것
  • atom이 업데이트 되면, 해당 atom을 구독하고 있던 모든 컴포넌트들의 state가 새로운 값으로 리렌더링 됨.
export const filterSelect = atom({
  key: 'filterSelect',
  default: {
    'start-date': dateConverter(new Date()),
    'end-date': dateConverter(new Date()),
    'weekly-date': dateConverter(new Date()),
    categories: '',
    subcategories: new Set(),
    seasons: new Set(),
    'serial-number': '',
    limit: 200,
    'deadline-week': dateConverter(setRecentSunday(new Date())),
  },
});

2. useRecoilState(todoListState)
useState()와 같이 배열의 첫 번째 원소가 상태, 두 번째 원소가 상태를 업데이트하는 함수를 반환하게 된다. 다른 파일에 있는 아톰을 읽을 수 있다.

먼저, 2가지 준비가 필요하다

useRecoilState Hook import하기
atom으로 만든 전역상태 import 하기

import filterSelect from 'file path' // atom으로 만든 전역상태
import {useRecoilState} from 'recoil' // 훅 import

const [state, setState] = useRecoilState(filterSelect); // 전역상태를 state로 만듦

useRecoilState는 useState와 사용법이 동일하다

위의 코드에서 처럼 useRecoilState 훅을 이용하여 state를 정의하고 그 내용으로는 훅의 인자로 전역상태를 넣어 전달한다.

이러한 과정은 useState 훅을 사용할 때 인자로 초기값을 넣어 전달하는 방법과 동일하다.

이제 state를 마치 컴포넌트 state 처럼 사용하면 된다.

전역 상태 수정하기
컴포넌트 state를 사용할 때는 setState 함수를 사용하여 state를 수정했다. Recoil도 이와 동일하다. 정의한 setState를 사용하여 전역상태를 수정하면 된다.

다만 조금 차이가 있다면 Recoil에서는 setState의 prev 상태가 수정 불가능하다.

예를들자면 이전의 컴포넌트 state에서는

setState(prev => {
prev.property1 = 1;
prev.property2 = 2;
return {...prev}
})

위의 방법을 써서 prev를 사용한 state의 수정이 가능했다.

하지만 Recoil에서는 prev를 수정할 수 없다.

이를 피해가는 방법으로는 prev를 새로운 변수에 전개연산자를 사용해 새로운 인스턴스를 정의해주는 것이다.

setState(prev => {
	const variable = {...prev};

	variable.property1 = ....;
	.
	.
	return {...variable}
}
)

useRecoilState 간단하게 사용하기
state를 정의할 때 일반적으로

const [state, setState] = useState();

방식으로 작성하는데, 만약 해당 컴포넌트에선 값을 읽기만 하고 수정할 일이 없다면 굳이 setState를 정의할 필요가 없다 이 경우

const [state] = useRecoilState() 처럼 setState를 생략하고 작성할 수 있다

만약 전역상태 값을 읽을 일은 없고 수정만 하면 된다면

const [, setState] = useRecoilState()

이때, 주의할 점은 쉼표 ( , )를 반드시 넣어줘야 한다는 점이다.

3. useRecoilValue(todoListState)
상태(atom)값만 필요할 경우에 사용하면 된다.

4. useSetRecoilState(todoListState)
상태(atom)를 업데이트하는 함수만 필요한 경우 사용하면 된다. setter 역할을 한다.

5. useResetRecoilState
아톰 값을 디폴트 값으로 변경시켜준다.


FaceBook Recoil 소개 영상

import { atom } from "recoil"

export const user = atom({
  key: "user",
  default: {
    id: "Admin",
    pwd: "Admin",
  },
});

export const counting = atom({
  key: "counting",
  default: 0,

});

위 코드를 살펴보면, recoil 에서 atom 을 import 해왔고, atom 이라는 함수에 key 와 default 로 이루어진 객체를 넣어줌으로써 atom 을 만들었습니다.

여기서 key 는 atom 을 구분해줄 고유의 값이며, default 는 해당 key 값을 가진 atom 의 기본값으로 설정해줄 value 를 넣어주면 됩니다.

이렇게 간단한 코드로 하나의 전역 상태를 만들었습니다.

import { useRecoilState } from "recoil";
import { counting } from "./store";

export function Example() {
  const [count, setCount] = useRecoilState(counting);
  const handleIncrease = () => {
    setCount((prev) => prev + 1);
  }
  return (
    <div>
      <span>{count}</span>
      <button onClick={handleIncrease}>increase</button>
    </div>
  );

}

위 코드를 살펴봤을 때 어떻게 atom 을 사용한 지 확인해보세요! 코드를 살펴봤을 때 useState 를 통해서 state 를 사용하는 모습과 비슷한 느낌입니다!

useRecoilState 라는 hook 을 recoil 라이브러리에서 가져와, 위에 정의한 atom 을 넣어주면서 값을 추적해 사용합니다.

이렇게 recoil 은 React 만을 위한 상태관리 라이브러리로서 가장 React 다운 상태관리 툴을 제공하고 있습니다. 그렇기에 useRecoilState 라는 간단한 hook 을 통해서 atom 을 가져와서 값을 추적하고, 값을 변경할 수 있는 것 입니다.

이렇게 짧은 과정을 통해서 전역 상태 관리를 하게되니, Redux 에서 사용했던 방식 (store 에 저장된 값을 가져오고, action 을 dispatch 해 reducer 를 통해서 state 를 변경 했던 과정) 에 아쉬움을 느끼게 됩니다.

import { useSetRecoilState, useRecoilValue } from "recoil";
import { counting } from "./store";

export function Example() {
  const setCount = useSetRecoilState(counting);
  const count = useRecoilValue(counting);

  const handleIncrease = () => {
    setCount((prev) => prev + 1);
  }

  return (
    <div>
      <span>{count}</span>
      <button onClick={handleIncrease}>increase</button>
    </div>
  );

위 컴포넌트와 별 다른 구조는 없지만 atom 과 atom 의 modifier 를 분리해서 사용할 수 있다는 것을 소개하고자 예제 코드를 작성했습니다. 각각 useRcoilValue, useSetRecoilState 를 활용하면 이들을 분리해서 사용할 수 있게 됩니다.

atom with typescript

//atom.ts

import { atom } from "recoil";

export interface IUser {
  id: string;
  pwd: string;
  name: string;
}

export const user = atom<IUser>({
  key: "user",
  default: {
    id: "admin",
    pwd: "admin",
    name: "관리자"
  }
});

먼저 atom 을 타이핑 하기 위해서 IUser 이라는 interface 를 선언하였고, 해당 interface 를 user 이라는 atom 에 typing 했습니다.

import { useRecoilState } from "recoil";
import { IUser, user } from "./atom";

export default function App() {
  const [LoginUser, setLoginUser] = useRecoilState<IUser>(user);
  return (
    <div>
      <p>userName: {LoginUser.name}</p>
      <p>userId: {LoginUser.id}</p>
      <p>userPwd: {LoginUser.pwd}</p>
    </div>
  );
}

컴포넌트에서 atom 을 사용하는데 있어서도 마찬가지로 IUser 인터페이스와 타이핑 했습니다. 이는 React 에서 typescript 를 적용해 useState 를 활용하는 방법과 거의 유사합니다.

6. selector
Recoil 을 공부 했을 때 atom 은 정말 쉽게 이해했지만, selector 라는 개념은 이해하기가 어려웠습니다. 그 이유는 atom 에 비해서 조금은 머릿속에 추상화 하기 어려웠기 때문이지 않을까 라는 생각을 해봅니다. 결국 selector 을 이해하기 위해서 추상화 했던 방법은 SQL 의 select 질의문을 활용해 봤습니다.

selector 은 atom 을 활용해 개발자가 원하는 대로 값을 뽑아서 사용할 수 있는 API 입니다. Recoil 의 공식 문서에서 selector 을 소개하는 문구를 살펴보겠습니다.


즉 “ atom 을 원하는 대로 변형해 값을 리턴받는다. ” 라고 생각할 수 있겠습니다. 이 과정을 마치 데이터베이스에서 저장된 데이터를 Select 을 통해 원하는 결과를 뽑아오는 과정으로도 유추 해볼 수 있겠다 라는 생각을 하였고, 이를 Select 과 연결하여 추상화 하니 이해하기 편했습니다.

파생된 상태는 다른 데이터에 의존하는 동적인 데이터를 만들 수 있는 개념입니다. 하나의 상태를 순수 함수인 selector에 전달하여 반환 받은 결과물에 대해 파생된 상태로 볼 수 있습니다.

또한 selector 은 readonly 한 값 만을 반환합니다. 따라서 Recoil 을 활용할 때 수정 가능한 값을 반환 받고자 한다면 반드시 atom 을 활용해야 합니다.

  • 아톰의 상태변화는 순수함수(selector)를 통해 일어남.
  • 셀렉터에서는 비동기처리 뿐만 아니라 데이터 캐싱 기능도 제공하기 때문에 비동기 데이터를 다루기에도 용이함.

Selector 구조

function selector<T>({
  key: string,

  get: ({
    get: GetRecoilValue,
    getCallback: GetCallback,
  }) => T | Promise<T> | RecoilValue<T>, // 타입 T에 해당하는 값, T를 리턴하는 Promise,

  set?: (
    {
      get: GetRecoilValue,
      set: SetRecoilState,
      reset: ResetRecoilState,
    },
    newValue: T | DefaultValue, // setter로 전달하는 값은 T 타입 값이어야 한다.
  ) => void,
}
)
  1. toDo 는 “DOING”, “DONE” 두 가지 상태를 가지고 있다. 현재 노출하기 원하는 상태는 atom 으로 관리한다.
  2. 생성된 toDo 는 현재 atom 에 배열로 담겨있다.
  3. 우리는 selector 을 통해서 status atom 을 추적하고, 원하는 상태의 값 만을 쏙쏙 골라올 것이다.
// atom.ts

import { atom, selector } from "recoil";

export type status = "DONE" | "DOING";

interface toDo {
  status: status;
  contents: string;
}

export const selectStatus = atom<status>({
  key: "nowStatus",
  default: "DOING"
});

export const toDos = atom<toDo[]>({
  key: "toDos",
  default: [
    { status: "DOING", contents: "default 1" },
    { status: "DONE", contents: "default 2" },
    { status: "DONE", contents: "default 3" },
    { status: "DOING", contents: "default 4" },
    { status: "DOING", contents: "default 5" }
  ]
});

export const selectToDo = selector<toDo[]>({
  key: "selectToDos",
  get: ({ get }) => {
    const originalToDos = get(toDos);
    const nowStatus = get(selectStatus);
    return originalToDos.filter((toDo) => toDo.status === nowStatus);
  }

});

위 코드를 살펴보면 toDos 라는 atom 에 toDo 배열을 담아놨고, selector 을 통해서 변화된 값을 리턴받아 사용하고 있습니다.

selector 의 구조를 살펴보면, atom 과 다른 부분이 있습니다. 위 코드에서 바로 get 이라는 코드를 살펴볼 수 있는데, selector 은 내부적으로 함수에서 get 을 반환 해주며 get 메서드를 활용해 현재 저장된 atom 이나 다른 selector 의 값을 받아올 수 있습니다. 이를 통해서 atom 을 input 받고 원하는 결과를 위해 배열을 변형해 output 해줍니다.

// App.tsx

import React from "react";
import { useRecoilState, useRecoilValue } from "recoil";
import { selectStatus, selectToDo, user } from "./atom";

export default function App() {
  const [status, setStatus] = useRecoilState(selectStatus);
  const selectToDos = useRecoilValue(selectToDo);

  const handleStatus = (event: React.ChangeEvent<HTMLSelectElement>) => {
    setStatus(event.currentTarget.value as any);
  };

  return (
    <>
      <div>
        <select value={status} onChange={handleStatus}>
          <option value="DOING">DOING</option>
          <option value="DONE">DONE</option>
        </select>
        <ul>
          {selectToDos.map((toDo, index) => {
            return (
              <li key={index}>
                <span>status: {toDo.status}</span>
                <br />
                <span>content: {toDo.contents}</span>
              </li>
            );
          })}
        </ul>
      </div>
    </>
  );

}

UI 컴포넌트 입니다. <select> 태그를 활용해 status atom 을 변경해주고 있으며, selector 을 통해서 toDo 배열을 화면에 뿌려주고 있습니다.

selector 의 또 다른 기능 set
selector 은 get 뿐만 아니라 set 을 활용해서 atom 의 값을 변경해줄 수 있습니다. 이를 통해서 비동기 통신 처리를 할 수도 있습니다. 간단히 위 toDo 예제를 활용해 set 을 활용한 모습을 살펴보겠습니다.

// atom.ts

export const selectToDo = selector<toDo[]>({
  key: "selectToDos",
  get: ({ get }) => {
    const originalToDos = get(toDos);
    const nowStatus = get(selectStatus);
    return originalToDos.filter((toDo) => toDo.status === nowStatus);
  },
  set: ({ set }, newToDo) => {
    set(toDos, newToDo);
  }
}
);

atom.ts 에서 변화한 코드는 selectToDo 의 set 부분입니다. get 을 활용 했을때와 마찬가지로 set 메서드를 인자로 받아와 사용합니다. 또한 인자로는 컴포넌트에서 넣어줄 newValue (여기서는 newToDo) 를 받을 수 있습니다. set 메서드의 첫 번째 매개변수로 변경할 atom 을 넣어주고 두 번째 인자로는 변경해 줄 값을 넣어줍니다.

import React, { useState } from "react";
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
import { selectStatus, selectToDo, toDo, toDos } from "./atom";

export default function App() {
  const [status, setStatus] = useRecoilState(selectStatus);
  const selectToDos = useRecoilValue(selectToDo);

  const toDoAtom = useRecoilValue(toDos);

  // 아래가 selector 의 set 을 위해 추가된 코드
  const [contents, setContents] = useState("");
  const setNewToDos = useSetRecoilState(selectToDo);

  const handleStatus = (event: React.ChangeEvent<HTMLSelectElement>) => {
    setStatus(event.currentTarget.value as any);
  };

  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setContents(event.currentTarget.value);
  };

  const handleSubmit = (event: React.ChangeEvent<HTMLFormElement>) => {
    event.preventDefault();
    if (contents === "") {
      return;
    } else {
      const newToDoList: toDo[] = [
        ...toDoAtom,
        {
          contents,
          status: "DOING"
        }
      ];

      setNewToDos(newToDoList);
      setContents("");
    }
  };

  return (
    <>
      <form onSubmit={handleSubmit}>
        <input value={contents} onChange={handleInputChange} />
        <button>Submit</button>
      </form>
      <br />
      <div>
        <select value={status} onChange={handleStatus}>
          <option value="DOING">DOING</option>
          <option value="DONE">DONE</option>
        </select>
        <ul>
          {selectToDos.map((toDo, index) => {
            return (
              <li key={index}>
                <span>status: {toDo.status}</span>
                <br />
                <span>content: {toDo.contents}</span>
              </li>
            );
          })}
        </ul>
      </div>
    </>
  );

}

컴포넌트 예제 코드입니다. 윗 예제의 코드들과 겹쳐서 긴 코드가 들어왔습니다. useSetRecoilState 훅을 통해서 selector 의 set 을 활용할 수 있는데, set 을 활용할 때는 인자로 변경할 값을 넣어주면 됩니다. <form> 의 submit 이벤트 내부에서 selector 의 set 을 통해서 toDos 라는 atom 을 변경해 줬습니다.

아래에서 이를 활용한 비동기 통신 예제를 살펴보도록 하겠습니다.

selector 을 활용한 비동기 통신
Recoil 을 활용해 비동기 통신을 하기 위해서는 selector 을 사용해야 합니다. 간단하게 코드를 통해서 알아보겠습니다.

export const selectId = atom({
  key: "selectId",
  default: 1
});

export const selectingUser = selector({
  key: "selectingUser",
  get: async ({ get }) => {
    const id = get(selectId);
    const user = await fetch(
      `https://jsonplaceholder.typicode.com/users/${id}`
    ).then((res) => res.json());
    return user;
  },
  set: ({ set }, newValue) => {
    set(nowUser as any, newValue);
  }

});

위 코드를 살펴보면 get 메서드를 통해서 API 콜을 하여서 데이터를 가져옵니다.

위 코드에서는 동적 parameter 을 위해서 id 라는 atom 을 사용합니다. 그럼 atom 을 사용하지 않고 컴포넌트에서 직접 파라미터를 넘겨주는 방법은 없을까요?

export const selectUser = selectorFamily({
  key: "selectOne",
  get: (id: number) => async () => {
    const user = fetch(
      `https://jsonplaceholder.typicode.com/users/${id}`
    ).then((res) => res.json());
    return user;
  }
});

// 컴포넌트에서 사용 시 

const user = useRecoilValue<IUser>(selectUser(id));

위 코드를 살펴보면 selectorFamily 를 활용해 비동기 통신을 하고 있는 것을 확인할 수 있습니다. 컴포넌트에서 동적 parameter 을 위해서 직접 매개변수를 넘겨주고 싶다면 selectorFamily 를 활용해 get 메서드 내부에서 직접 파라미터를 받아줄 수 있습니다.

selector 의 강력한 기능
selector 을 활용해 비동기 통신을 하였을 때 체감되는 강력한 기능은 캐싱 입니다. selector 을 활용해 비동기 통신을 해온다면, 내부적으로 알아서 값을 캐싱 해주기 때문에 이미 한번 비동기 통신을 하여 값이 캐싱되어 있다면 매번 같은 비동기 통신을 하지 않고 캐싱 된 값을 추적하여 사용합니다.

Suspense Trouble Shooting

위 에러는 비동기 통신을 할 때 데이터가 아직 도착하지 않았을 경우 (isLoading 상태) 대체해서 보여줄 UI 가 없다는 뜻입니다. 이를 해결하기 위해서는 아래 세 가지 방법이 있습니다.

  1. <React.Suspense> 활용
    가장 보편적인 방법입니다. index.tsx 에서 <App> 컴포넌트를 <Suspense /> 태그로 감싸줬습니다.
    root.render( <RecoilRoot> <Suspense fallback={<div>loading..</div>}> <App /> </Suspense> </RecoilRoot> );
  2. Recoil 의 Loadable 활용
    Recoil 에서는 atom 이나 selector 의 현 상태를 나타낼 수 있는 hook 을 제공합니다.
    const userLoadable = useRecoilValueLoadable(getUser);
    Loadable 을 객체는 hasValue , hasError , loading 세 가지 상태를 가지고 있습니다. 이에 따라서 컴포넌트의 상태를 표현해 줄 수 있습니다.
  3. startTransition 활용
    이번 React 18 버전에서 Transition 이라는 기능이 나왔습니다. Transition 에 관한 내용은 아래 블로그 글에서 확인하시면 좋을 것 같습니다.
    바로가기 >

사용이유

1. Redux의 복잡한 코드
Redux 를 사용하고자 할 때 마주하는 가장 큰 어려움은 복잡한 코드다. Redux 를 활용하기 위해서는 action, dispatcher, reducer, store 등 구현해야 할 기본 코드 들이 큰 편이다. 이는 보일러 플레이트를 활용해서 해결할 수 있는 문제지만, 만약 여러 개발자가 공동 작업 할 때 컨벤션을 적용하지 않고 코드를 작성할 경우 자기만 알아볼 수 있는 구조의 코드를 작성하게 된다.
2. 간단한 Recoil 의 개념
Redux 를 이해하고 사용하려면 공부해야 할 것들이 많다. 데이터의 흐름을 추상화 하여서 익히려고 하여도 여러가지 복잡한 흐름을 이해하는 건 쉽지 않다. 이에 반해서 Recoil 에서 state 를 관리하는 방법은 굉장히 간단해 보인다.
3. 쉽게 사용하는 비동기 로직
Redux 에서 비동기를 활용하기 위해서는 middleware 을 활용한다. 비동기 통신
을 한다면 통신의 결과가 Success 일수도 있고 Fail 일 수도 있다. 이를 구분하여 state 관리를 해야하는데, 이를 쉽게 하기 위해서 Redux 에서는 Redux-thunk 혹은 Redux-saga 같은 미들웨어를 활용한다. 하지만 Recoil 에서는 내장된 개념인 selector 을 활용해 추가적인 미들웨어의 사용 없이 쉽게 비동기 로직을 구현할 수 있다.

장점

  • Hook을 사용한 React 개발자라면 쉽게 적응 가능.
  • 전역 state라는 개념만 이해한다면 atom정도는 쉽게 활용.

단점

  • 안정성에 대한 걱정
    레포지토리가 facebook/Recoil이 아니고, facebookexperimental/Recoil.
  • 실험적인 API들
    recoil의 api중에는 아직도 _UNSAFE surfix가 붙은것이 많음.
    하지만 빠른 속도로 UNSAFE 딱지가 떼지고 있음.
  • devtools 부재
    리덕스의 devtools 만큼 훌륭한 디버깅툴이 아직 없음.
    비공식 devtools 플러그인이 있기는 한데 퀄리티가 좋지는 않음.
  • 출시된지 얼마 안된 기술이기에 생태계가 적음.
    atom, selector에 대한 기본적인 예제 이외에는 소개된 곳이 적음. 공식문서의 예제에 의존해야 하고 어떤식으로 써야하는 정형화된 구조나 패턴이 없어 아직은 큰 규모의 프로젝트에 도입하기가 조금 꺼려질 수 있음.

Recoil With Storage(feat. effects)
Recoil 정확하게 사용하기!(feat.selector)
React-Query 도입을 위한 고민(feat.Recoil)

MobX

Redux와 다른 상태 관리 라이브러리. 객체 지향적인 특징

장점

  • 러닝커브가 높지 않음
  • Redux와 달리 불변성에는 신경써주지 않아도 될 정도로 규칙만 잘 신경써준다면 최적화가 잘 됨.
  • 보일러플레이트 코드의 양 또한 적기 때문에 프로젝트의 규모가 크지 않다면 추천.

단점

  • 리덕스와 달리 store가 여러 개가 될 수 있음. 분리가 용이해 편리할 수도 있는 반면 상태 변경시 다수의 스토어가 영향을 받을 수 있음.
  • 리덕스와 다르게 스토어의 데이터를 액션의 발행없이 업데이트할 수 있음. 이는 구현은 쉽고 용이하지만 테스트나 유지보수의 측면에서는 문제를 일으킬 수도 있음.
  • 장기적인 프로젝트, 유지보수 및 확장성을 고려해야 하는 프로젝트의 경우에는 MobX는 좋지 않은 선택.
  • 사용률은 Redux에 비해 현저히 부족

ContextAPI

React에서만 사용할 수 있음. (리액트 내장 기능)
Redux와 다르게 React에서만 사용할 수 있음. 리덕스와 다르게 여러 저장소가 존재할 수 있음.
Context API는 크게 전역 상태가 저장되는 context, 전역 상태를 제공하는 Provider, 그리고 전역 상태를 받아 사용하는 Consumer로 나뉘어져 있음.

장점

  • 네이티브 리액트만으로 전역 상태관리가 가능.
  • 부모와 자식간 props를 날려 state를 변화시켰던 react 방식과는 달리 context api는 컴포넌트 간 간격이 없음. 즉, 컴포넌트를 건너띄고 다른 컴포넌트에서 state, function을 사용할 수 있음.

단점

  • 리덕스에서의 강력한 미들웨어 기능은 없음.
  • 상태를 넘겨줄 때 상태가 여러 개라면 Provider를 중첩해서 내려 줘야 함.

Reference
https://velog.io/@danmin20/%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EB%AD%98-%EC%93%B8%EA%B9%8C

https://developer.mozilla.org/ko/docs/Glossary/MVC

https://velog.io/@alskt0419/FLUX-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90%EB%9E%80

https://devlog-h.tistory.com/26

https://velog.io/@juno7803/Recoil-Recoil-200-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0

https://velog.io/@gomjellie/3%EB%B6%84-Recoil

https://velog.io/@seohyunsim/context-api%EB%9E%80

https://javascriptpatterns.vercel.app/patterns/design-patterns/proxy-pattern

https://velog.io/@yujeong136/%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EC%99%80-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%A0%95%EB%A6%AC

https://velog.io/@yoho98/%EB%B0%94%EC%9D%B8%EB%94%A9binding

https://velog.io/@sunaaank/data-binding

https://velog.io/@kimju0913/%EB%8B%A8%EB%B0%A9%ED%96%A5-%EB%B0%94%EC%9D%B8%EB%94%A9%EA%B3%BC-%EC%96%91%EB%B0%A9%ED%96%A5-%EB%B0%94%EC%9D%B8%EB%94%A9

https://adjh54.tistory.com/49#--%--%EC%--%--%EB%B-%A-%ED%--%A-%--%EB%-D%B-%EC%-D%B-%ED%--%B-%--%EB%B-%--%EC%-D%B-%EB%--%A--Two-way%--Data%--Binding-%EC%-D%B-%EB%-E%--%-F

https://talkwithcode.tistory.com/6

https://blog.hyunmin.dev/15

https://medium.com/hcleedev/web-react-flux-%ED%8C%A8%ED%84%B4-88d6caa13b5b

https://beomy.tistory.com/44

https://hanamon.kr/redux%EB%9E%80-%EB%A6%AC%EB%8D%95%EC%8A%A4-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC/

https://react.vlpt.us/redux/

https://velopert.com/3528

https://chanhuiseok.github.io/posts/redux-1/

https://tech.osci.kr/2022/06/16/recoil-state-management-of-react/

https://code-masterjung.tistory.com/128

https://velog.io/@myway_7/Recoil%EA%B8%B0%EC%B4%88-Recoil-%EC%82%AC%EC%9A%A9%EB%B2%95

https://velog.io/@hwanieee/Recoil-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0

https://freestrokes.tistory.com/165

profile
선명한 기억보다 흐릿한 메모

0개의 댓글