.
본문 바로가기
리액트

Redux는 왜 쓰는 걸까?(1)

by 와칸다개발자 2022. 2. 21.

Ref

다음 글들을 읽고 나름 제 생각을 정리한 것입니다.

https://blog.isquaredsoftware.com/2021/01/context-redux-differences/#using-context

 

Blogged Answers: Why React Context is Not a "State Management" Tool (and Why It Doesn't Replace Redux)

Definitive answers and clarification on the purpose and use cases for Context and Redux

blog.isquaredsoftware.com

 

 

Redux 는 왜 쓰는 걸까?

 

항상 기술을 사용할 때 이 기술이 어떤 문제를 해결하기 위해 나왔는지 아는 것은 매우 중요하다.

 

문득 궁금해졌다. 

 

누군가 나에게 리덕스를 왜 쓰냐고 물어본다면 props drilling을 피하기 위해서라고 대답할 것이다.

 

그러면 Context API 쓰면 되지 않음??? 이렇게 질문이 들어온다면 대답을 하기 어려울 것 같다.

 

내가 참조한 블로그 글에서는 다음과 같이 비교하고 있다.

 

Redux Context
아무것도 저장하거나 관리 하지 않는다. 상태를 관리하고 저장한다.
오직 리액트 컴포넌트에서만 작동한다. 리액트 이외에 다른 프레임워크에서도 사용가능 하다.
원시 객체, 객체, 배열 등 전달한다. 단일 값을 읽을 수 있다.
props drilling을 피하기 위해 사용한다. props drilling을 피하기 위해 사용한다.
시간이 지남에 따라 값이 어떻게 변경되는지 알 수 없다. 동작 및 시간 경과에 따른 상태 변경 기록을 보여주는
DevTools가 있다. 
(크롬 확장프로그램 Redux dvetool 말하는 것 같다)
컨텍스트 값이 변경될 때 업데이트를 막을 수 없다.
Redux Store에서 읽고 싶은 값만 읽고 해당 값이 변경될 때만 렌더링 한다.

이렇게만 비교하면서 보면 지엽적이고 와닿지는 않는다.  역시 코드를 봐야 한다.

필자가 제일 중요하다고 생각되는 부분은 맨 마지막 부분이다.

 

Context API

 

일단 정의부터 보자. 리액트 공식문서에서는 다음과 같이 정의하고 있다.

 

context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에
데이터를 제공할 수 있습니다.

일반적인 React 애플리케이션에서 데이터는 위에서 아래로 (즉, 부모로부터 자식에게) props를 통해 전달되지만, 애플리케이션 안의 여러 컴포넌트들에 전해줘야 하는 props의 경우 (예를 들면 선호 로케일, UI 테마) 이 과정이 번거로울 수 있습니다. context를 이용하면, 트리 단계마다 명시적으로 props를 넘겨주지 않아도 많은 컴포넌트가 이러한 값을 공유하도록 할 수 있습니다.

 

목적이 분명히 쓰여있다. props drilling을 피하기 위한 것.

 

Context API는 상태 관리 도구가 아니다. 단지 데이터의 전달 공유만 할 뿐이다.

 

상태 관리 도구가 아니라니.. 좀 당황스럽다. 그러면 상태 관리란 도대체 무엇일까? 공식 DOC에서 긁어왔다.

 

props (short for “properties”) and state are both plain JavaScript objects. While both hold information that influences the output of render, they are different in one important way: props get passed to the component (similar to function parameters) whereas state is managed within the component (similar to variables declared within a function).

 

컴포넌트 안에서 관리되는 상태 , 렌더링에 영향을 주는 일종의 정보 및 데이터라고 쓰여 있다.

 

Context API를 간단히 사용해보자

 

props 전달을 하지 않고 값을 출력하는 간단한 프로그램을 작성하겠다.

 

ContextStore.tsx

import React, { createContext, useState } from "react";
interface IContext {
data: number;
constantData: number;
setData: React.Dispatch<React.SetStateAction<number>>;
}
interface Props {
children: JSX.Element | JSX.Element[];
}
export const ArticleContext = createContext<IContext>({
data: 0,
constantData: -1,
setData: () => { }
});
export default function ContextStore({ children }: Props) {
const [data, setData] = useState<number>(0);
return (
<>
<ArticleContext.Provider
value={{
data,
constantData: 23,
setData
}}
>
{children}
</ArticleContext.Provider>
</>
)
}

간단한 context 컴포넌트를 작성했다.

App.tsx

import { useState, createContext, useContext } from 'react';
import LeftChild from './component/leftchild';
import RightChild from './component/rightchild';
import Parent from './component/parent';
import styled from 'styled-components';
import ContextStore from './context';
const ChildWrapper = styled.div`
height:100px;
width:100%;
display:flex;
`
export default function GrandParent(): JSX.Element {
return (
<>
<ContextStore>
<h1 style={{ textAlign: 'center' }}>grand parent component</h1>
<Parent />
<ChildWrapper>
<LeftChild />
<RightChild />
</ChildWrapper>
</ContextStore>
</>
)
}

ContextStore를 반드시 가장 바깥 컴포넌트에 감싼다. 

 

Parent.tsx

import React, { useContext } from 'react';
import { ArticleContext } from '../context';
export default function ParentComponent() {
const { constantData } = useContext(ArticleContext);
console.log('parent가 렌더링 된다 ~~');
return (
<>
<h1 style={{ textAlign: 'center' }}> parent component </h1>
</>
)
}

parent 컴포넌트는 단지 props로 인자를 넘겨주지 않아도 값을 공유할 수 있도록 보여주는 컴포넌트

 

저 constantData를 나중에 잘 보자

 

LeftChild.tsx

import React, { useState, useContext } from 'react';
import styled from 'styled-components';
import { ArticleContext } from '../context';
const StyleLeftChild = styled.span`
width:50%;
height:100%;
border:2px solid green;
text-align:center;
`;
export default function LeftChild() {
const { data, setData } = useContext(ArticleContext);
console.log('왼자식 렌더링 ~~');
return (
<>
<StyleLeftChild>
<h2>leftChild: {data} </h2>
<button onClick = {() => setData(data + 1)}>inc</button>
<button onClick = {() => setData(data - 1)}>dec</button>
</StyleLeftChild>
</>
)
}

Context Store를 구독하는 왼쪽 자식 컴포넌트다. useContext를 사용하여 Context Store에 값을 불러온다.

 

RightChild.tsx

import React, { useState, useContext } from 'react';
import styled from 'styled-components';
import { ArticleContext } from '../context';
const StyleRightChild = styled.span`
width:50%;
height:100%;
border:2px solid blue;
text-align:center;
`;
export default function RightChild() {
const { data } = useContext(ArticleContext);
console.log('오른자식 렌더링 ~~');
return (
<>
<StyleRightChild>
<h2>rightChild: {data * 2} </h2>
</StyleRightChild>
</>
)
}

Context Store의 값을 구독하는 또 다른 자식 컴포넌트 

 

결괏값은 이렇다.

 

Context Store가 전체 App을 감싸고 자식 컴포넌트는 언제든지 Context Store에 값을 구독하며 사용할 수 있다.

 

이로서 Context Store는 그 어떠한 것도 관리를 하지 않는다 그저 Store에 구겨 넣어서 자식 컴포넌트에 전달하는

 

파이프라인 목적만 할 뿐 Context Store 안에서 값을 업데이트하거나 변경하거나 하지 않는다.

 

Context API의 명확한 목적은 props-drilling을 피하기 위한 것

 

Context API는 성능 이슈가 있다. 이제 이 프로그램을 작동 시키면 다음과 같이 렌더링 된다.

 

import React, { useContext } from 'react';
import { ArticleContext } from '../context';
export default function ParentComponent() {
const { constantData } = useContext(ArticleContext);
console.log('parent가 렌더링 된다 ~~');
return (
<>
<h1 style={{ textAlign: 'center' }}> parent component </h1>
</>
)
}

parent component는 Context Store에 constantData를 구독하고 있지만 값이 변경되지 않음에도 불구하고

 

항상 렌더링을 유발한다. 점점 커지는 현대 웹 서비스에서는 굳이 쓸데없는 리렌더링은 당연히 좋지 않다. 

 

Redux

Redux의 정의나 설명은 여기서 작성하기 힘들겠다. 

이제 Redux로 똑같이 코드를 작성해 보겠다.

App.tsx

import LeftChild from './component/leftchild';
import RightChild from './component/rightchild';
import Parent from './component/parent';
import styled from 'styled-components';
import { Provider } from 'react-redux';
import { createStore, Store } from 'redux';
import rootReducer from './reducer/index';
import './index.css';
const store: Store = createStore(rootReducer);
const ChildWrapper = styled.div`
height:100px;
width:100%;
display:flex;
`
export default function GrandParent(): JSX.Element {
return (
<>
<Provider store={store}>
<h1 style={{ textAlign: 'center' }}>grand parent component</h1>
<Parent />
<ChildWrapper>
<LeftChild />
<RightChild />
</ChildWrapper>
</Provider>
</>
)
}

reducer/index.ts

import { combineReducers } from 'redux';
import counterReducer, { CountState } from './counter';
export interface RootState {
count : CountState,
}
export default combineReducers<RootState>({
count: counterReducer,
});

reducer/counter.ts

export const INCREASECOUNT = 'counter/INCREASECOUNT' as const;
export const DECREASECOUNT = 'counter/DECREASECOUNT' as const;
export interface CountState {
data: number;
constatnt: number;
};
interface CountAction {
type: string;
};
const initialState: CountState = {
data: 0,
constatnt: 23
};
export const increment = () => ({
type: INCREASECOUNT,
})
export const decrement = () => ({
type: DECREASECOUNT,
})
// 리듀서
export default function counterReducer(state: CountState = initialState, action: CountAction): CountState {
switch (action.type) {
case INCREASECOUNT:
return {
...state,
data: state.data + 1
};
case DECREASECOUNT:
return {
...state,
data: state.data - 1
};
default:
return state;
}
}

 

ParentComponent.tsx

import { useContext } from 'react';
import { useSelector, shallowEqual } from 'react-redux';
import { RootState } from '../reducer/index';
import { CountState } from '../reducer/counter';
import { ArticleContext } from '../context';
export default function ParentComponent() {
const constantData = useSelector((state: RootState) => state.count.constatnt);
console.log('parent가 렌더링 된다 ~~');
return (
<>
<h1 style={{ textAlign: 'center' }}> parent component {constantData} </h1>
</>
)
}
export function ParentComponent2() {
const { constantData } = useContext(ArticleContext);
console.log('parent가 렌더링 된다 ~~');
return (
<>
<h1 style={{ textAlign: 'center' }}> parent component: {constantData} </h1>
</>
)
}

 

LeftChild.tsx

import React, { useState, useContext } from 'react';
import styled from 'styled-components';
import { useDispatch, useSelector } from 'react-redux';
import { increment, decrement } from '../reducer/counter';
import { RootState } from '../reducer/index';
import { ArticleContext } from '../context';
const StyleLeftChild = styled.span`
width:50%;
height:100%;
border:2px solid green;
text-align:center;
`;
export default function LeftChild() {
const dispatch = useDispatch();
const data = useSelector((state: RootState) => state.count.data);
console.log('왼자식 렌더링 ~~');
return (
<>
<StyleLeftChild>
<h2>leftChild: {data} </h2>
<button onClick={() => dispatch(increment())}>inc</button>
<button onClick={() => dispatch(decrement())}>dec</button>
</StyleLeftChild>
</>
)
}

 

RightChild.tsx

import React, { useState, useContext } from 'react';
import styled from 'styled-components';
import { useSelector } from 'react-redux';
import { RootState } from '../reducer/index';
import { ArticleContext } from '../context';
const StyleRightChild = styled.span`
width:50%;
height:100%;
border:2px solid blue;
text-align:center;
`;
export default function RightChild() {
const data = useSelector((state: RootState) => state.count.data);
console.log('오른자식 렌더링 ~~');
return (
<>
<StyleRightChild>
<h2>rightChild: {data * 2} </h2>
</StyleRightChild>
</>
)
}

 

Redux Store를 구독하고 있는 컴포넌트는 데이터를 가져올 때 useSelector hook을 사용한다.

 

Redux 의 큰 장점은 변경되지 않는 값에 대해 리렌더링을 유발하지 않는다는 것이다. 결과를 확인해보자.

 

2편에 계속..

댓글