[SEB_FE_45] 2023.06.21 / Redux & Reduxjs/toolkit & React-redux

Kay·2023년 6월 23일
0

45기 교육생분들 중 한분께서 자료 공유방에 Redux toolkit에 대해 학습할 때 도움이 되었던 유튜브 영상 하나를 공유해주셨는데,
매우 도움이 되어 영상 내용을 간단히 정리하고자 합니다.

유튜브 영상: Let’s Learn Modern Redux! (with Mark Erikson) — Learn With Jason

(영어를 잘 하지 못하기 때문에 오역 있을 수 있습니다🥲
또한 중간중간 내용이 많이 생략되어있습니다🥲

오역 혹은 오타에 대한 피드백 주시면 수정하도록 하겠습니다!

Redux의 옛날 코드 혹은 Flux의 기본 개념에 대해 학습하신 후 영상 혹은 글을 보시는 것을 추천드리며,
글을 읽으실 분들은 참고하여 글을 읽어주시고 영상 보시는 것을 더욱 추천드립니다!)

두 분이 등장하는데, 한 분은 진행을 담당해주시는 Json Lengtorf(이하 J)와 한 분은 Redux 운영자(Maintainer of Redux)인 Mark Erikson(이하 M)이다.

Redux에 대한 소개 및 간단한 Q&A

J: Redux를 처음 들어보거나 친숙하지 않은 분들을 위해 Redux에 대해 소개해 주시겠어요?

M: Redux는 예측 가능한 자바스크립트 어플리케이션의 상태(이하 state) 컨테이너입니다. 다시 말하자면, Redux는 어플리케이션의 글로벌 state를 구조화하기 위한 하나의 패턴이자 라이브러리입니다. (React나 UI 라이브러리에 종속되어 있지 않습니다.)

어플리케이션 내의 state가 어떻게 업데이트되는지 한 눈에 볼 수 있도록 하며, state가 어떻게, 언제, 왜 업데이트 되었는지 추적할 수 있게 합니다.

Redux의 목적은 어플리케이션이 어떻게 행동하도록 의도되어 있는지 그리고 어떻게 행동하는지에 대해 이해하는데에 사용자로 하여금 자신감을 가지도록 하는 것에 있습니다.

J: 하지만 혹자는 Redux의 보일러플레이트가 과도하다고 말합니다.

M: 초기 Redux 공식 문서에 다음과 같은 내용이 있었습니다. "Redux는 코드를 짧은 방법으로 작성되도록 의도된 것이 아닙니다. 이 의미는 당신의 코드를 예측가능하게 한다는 것입니다."

저는 이에 대해 언급할 때 "내재된 복잡함(inherent complexity)"와 "자연스럽게 따라오는 복잡함(incidental complexity)"을 구분지으려 합니다.

더 많은 일을 한다는 것, 그것은 Redux가 디자인된 방법입니다.

Redux의 프로세스(process)는 action objects를 정의하고, 그들을 dispatch하고, 그 코드를 분리되게 작성하는 것입니다.

그리고 중요한 점은 어플리케이션의 state를 (불변)immutable하게 업데이트해야 한다는 점입니다. (object나 배열을 카피하거나)
하지만 그 부분은 자바스크립트가 기본적으로 제공하는 것이 아니기 때문에 사용자에게 노력을 요구합니다.

하지만 Redux는 그 당시 Flux 패턴의 라이브러리들 중 가장 최고였습니다.
사실 React의 prop-drilling을 피하기 위한 가장 좋은 방법은 아니었습니다만, 많은 사람들이 이 문제를 해결하기 위해 redux를 많이 선택하였습니다.

그 이후로 많은 변화가 있었습니다.
React는 새로운 context API hook이 prop-drilling을 피하기 위해 만들어졌고, 많은 다른 좋은 라이브러리들이 생겨났습니다.

하지만 이상하게도 redux는 많은 사용자들에게 사용되어왔습니다.
(If you have a hammer, everything you see is a nail :) )

Redux에 대해 처음 접한 사람들은 redux에 대한 튜토리얼을 작성하며,
"If you're using react, you have to be using Redux!!!"
라는 문구를 넣었습니다.

redux가 필요하지 않은 소규모 프로젝트에서도 redux를 적용하게 되었습니다.😮

"저는 모든 사람들이 Redux를 사용하길 바라는 것이 아닙니다. Redux는 모든 상황에 적합한 라이브러리는 아닙니다.

사용자들이 trade-offs를 고려하고 이 라이브러리가 불러올 차이점을 고려하고, 그들이 해결해야 할 진정한 문제를 생각하고나서 그 문제를 해결하기 위한 라이브러리를 선택하길 바랍니다."

J: boilerplate의 목적은 무엇인가요? boilerplate가 다음에 어떤 일이 일어날지 예측하게 해주는 것은 아닌 것 같습니다만,, 우리는 그저 우리가 코드를 예측가능하게 하기 위해 작성합니다.🥲

(아마 모든 사람들이 그저 보일러플레이트 코드를 작성하는데 그 목적은 서로간에 predictable함을 위해서만 작성할 뿐이라고 말씀하신 것 같습니다!)

M: 말씀하신 부분은 좋은 예시라고 생각하는데요.
근래 몇년간 튜토리얼을 보신 분들은 switch statement를 사용한 것을 보셨을텐데요.
사람들: (👥👤👥👤뭐야...👥👤👥👤웅성웅성...) 왜 swtich statement를 사용해야하는거지?

Redux는 action.type의 current value에만 관심이 있었습니다.
여러개의 action.type을 한눈에 볼 수 있도록 할 수 있는 문법, 그것은 switch statement였습니다!

사람들: (👥👤👥👤뭐야...👥👤👥👤웅성웅성...) switch statment를 써야하나요?

아닙니다. if else 문을 쓸 수도 있습니다. switch statement는 여러개의 action.type이 분기되는 것을 명확히 보여줄 수 있는 방법이었을 뿐, 필요한 것은 아닙니다.

react-redux & redux/toolkit with Typescript! 코드를 작성하며 알아봅시다!

J: Redux의 이점을 가져가면서 boilerplate 코드를 줄일 수 있는 것을 소개하기 위해 나왔죠?

M: 이미 많은 분들이 보셨겠지만 2018년에 state를 다루기 위한 React hook이 발표되었고, 그 후 사람들은 "Redux? hook? when?!!!"이라고 물어봤죠 :)

저희는 2019년에 react-redux 라이브러리를 출시했고 이전 API 또한 유지하고 있습니다.

vite를 사용하여 프로젝트를 우선 세팅하여 봅시다!

Vite (이건 다음 프로젝트에 적용하면서 공부하여 포스팅하기로 하겠습니다!)

(~ 프로젝트 세팅 중 ~)



(우와! 정말 라이브러리 이름처럼 짱짱 빠르네요!)
기본 템플릿은 Create-react-app과 거의 비슷하지만,
한가지 다른점은 entry file이 main.tsx라는 점입니다!

folder 구조

M: 기존에 redux를 사용하는 사용자들은 actions, reducers, types라는 폴더를 사용했지만 features라는 폴더 안에 모든 관련된 코드를 작성할 수 있습니다.

(features라는 폴더를 꼭 사용해야하는 것은 아니고 redux/toolkit을 사용하면 한 폴더 안에 관련된 코드를 다 작성할 수 있음을 얘기하는 것 같습니다!)

기존에는 action type, action, reducer를 나누어 작성했지만 이번엔 한 파일안에 모든 로직을 작성하는 것이 권장되는 바입니다.

우리는 통상적으로 "sliced file"이라고 부르며 redux pattern을 one slice에 모든 로직과 데이터를 포함하는 것이기 때문입니다. 이는 Dux? Ducks? pattern이라고 부릅니다.
(redux에서 re를 뺀 ducks "quack quack"이라고 하는 것으로 보아 dux와 duck이 발음이 비슷해서 약간 쪼크로서 내부적으로 부르는 듯 싶습니다!)

createSlice

createSlice는 reduxjs/toolkit의 main API 함수이며, Redux Logic과 Payload Action을 타입스크립트 형태로 정의하기 위함입니다.

createSlice를 사용하면 불변성, action 함수를 고려할 필요가 없습니다!
(왜냐면 얘네들이 다 해주기 때문!)

features/counterSlice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface CounterState {
	value: number;
}

const initialState: CounterState = {
	value: 0,
}

// 첫번째 파라미터: 해당 Slice의 이름
// 두번째 파라미터: 초기 State
// 세번째 파라미터: reducer
const counterSlice = createSlice({
	name: 'counter',
  	initialState,
  	reducers: {
   	  // increment
      // state의 불변성을 신경쓸 필요없이 코드를 작성하면 되고,
      // return 문이 필요하지 않음..!!!
      // 왜냐면 redux toolkit 라이브러리는 immer라는 라이브러리를 쓰기 때문!
      // createSlice 내에서 자체적으로 state를 복사하고 안정적인 형태로 반환하는 역할을 함!
      
      // it's okay to do this because immer makes it immutable
      // under the hood
      incremented(state) {
        state.value++;
      }
    }
})

export const { incremented } = counterSlice.actions;
export default counterSlice.reducer;

위 코드가 우리가 redux slice라고 부르는 basic shape입니다.

configureStore

configureStore는 basic redux create store 함수입니다.
기본적으로 하나의 store를 만들기 위한 setup이며, Redux Dev Tools를 기본적으로 켜고, 자동으로 Thunk Middlewar를 추가하며, 기본 실수 혹은 문제를 잡을 development checks를 켭니다.
(와우~)

app/store.js

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../featrues/counters/counterSlice';

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

export type AppDispatch = typeof store.dispatch;
// 다음은 타입스크립트 매직~!
export type RootState = ReturnType<typeof store.getState>;

M: 커서를 RootState 위에 올려 무슨 타입인지 확인해볼까요?

J: 오~!

M: RootState는 counterState를 value로써 담고 있는 counter key를 감싼 객체(Ojbect)입니다!

이 부분은 redux tookit 라이브러리의 영역이 아닌
타입스크립트 인터페이스(interface)를 사용한 것으로 이것 덕분에 rootState에 대해 정의할 필요가 없습니다!

J: 오~ 엄청 좋네요~!

react-redux provider 세팅

M: 앞으로 두가지 더 세팅해봅시다!
main.tsx로 이동해서 store와 프로젝트를 연결하기 위한 react-redux provider를 설정을 해봅시다.

main.tsx

// 중략...
import { Provider } from 'react-redux;
import { store } from './app/store';
// 중략 ...

ReactDOM.render(
	<React.StrictMode>
  		<Provider store={store}>
  			<App />
  		</Provider>
  	</React.StrictMode>
)

redux devtools extension

redux를 접한 많은 분들이 알고 계실 redux devtools extension!

타입스크립트와 redux를 사용하는 분들에게 추천하는 hooks 패턴

M: app 폴더에 hooks.ts 파일을 만들어보세요!
typescript를 사용하는 분들을 위해 새롭게 추천하는 패턴입니다.

React-redux는 hook을 제공하고, 타입스크립트는 그 hook이 어떻게 작동하는지 알지만

그 hook은 우리의 own application의 특정 state 또는 dispatch capability에 대해서는 알지 못합니다.

그래서 저희는 application의 state와 dispatch의 올바른 타입을 알고있는 React-redux hooks를 미리 정의하는 가장 좋은 방법을 찾았습니다.

app/hooks.ts

import {
	TypedUseSelectorHook,
  	useDispatch,
  	useSelector
} from 'react-redux';
import { RootState, AppDispatch } from '@/store';

export const useAppDispatch = () => useDispatch<AppDispatch>();

// useSelector 함수를 타입과 함께 alias 한 것
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

이렇게 작성하면
타입스크립트 타입을 정확히 작성한 두가지 hook variable를 export하고 있고

기존에 useSelector, useDispatch를 사용하는 곳에 위 두가지 hook을 사용할 겁니다.

컴포넌트에 적용!

App.tsx

// 중략 ...
import { useAppDispatch, useAppSelector } from './app/hooks';
import { incremented } from './features/counter/counter-slice';
// 중략 ...

function App() {
  const count = useAppSelector((state) => state.counter.value);
  const dispatch = useAppDispatch();
  
  const handleClick = () => {
  	dispatch(incremented());
  }
	
  return (
	<button onClick={handleClick}>count is: {count}</button>
  )
}

store에서 가져온 state인 value의 타입이 잘 적용되어 있습니다!

dispatch 함수의 타입 또한 아주 잘 작성되어 있습니다!

버튼을 눌러 redux devtools를 통해 state의 변화(diff)를 확인할 수 있습니다!

각 state 변화에 따른 화면의 변화를 보고 싶다면
JUMP 버튼을 통해 time travel도 할 수 있습니다!

M: action types를 보면 incremented 앞에 counter라는 단어가 포함되어 있는 것을 볼 수 있는데
어디서 왔을까요~😎

createSlice의 name을 counter로 지정하였는데,
그것과 reducer 함수 이름을 참고하여
자동으로 action type string을 만들 때 포함시키도록 되어있습니다!~😎 캬아~!

action에 payload 지정하기

한가지만 더 해봅시다!
이번엔 숫자를 정하여 그 숫자를 커지게끔하는 action 함수입니다.

features/counterSlice.ts

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface CounterState {
	value: number;
}

const initialState: CounterState = {
	value: 0,
}

// 첫번째 파라미터: 해당 Slice의 이름
// 두번째 파라미터: 초기 State
// 세번째 파라미터: reducer
const counterSlice = createSlice({
	name: 'counter',
  	initialState,
  	reducers: {
      incremented(state) {
        state.value++;
      }
      // 요기부터 새로 작성
      amountAdded(state, action: PayloadAction<number>) {
			state.value += action.payload;
		}
      
    }
})

export const { incremented, amountAdded } = counterSlice.actions;
export default counterSlice.reducer;

App.tsx

// 중략 ...
import { useAppDispatch, useAppSelector } from './app/hooks';
import { incremented, amountAdded } from './features/counter/counter-slice';
// 중략 ...

function App() {
  const count = useAppSelector((state) => state.counter.value);
  const dispatch = useAppDispatch();
  
  const handleClick = () => {
    // 요기 수정
  	dispatch(amountAdded(3));
  }
	
  return (
	<button onClick={handleClick}>count is: {count}</button>
  )
}

마무리

M: redux를 사용하는 것은 동일하지만, 위 코드를 참고하여 프로젝트에 적용한다면 지난 몇년과는 전혀 다른 코드를 작성하게 될 것입니다.😎

0개의 댓글