728x90

react 에서 redux-toolkit 을 사용하기 위해서는 '@reduxjs/toolkit' 과 'react-redux' package를 설치해줘야한다.

npm install @reduxjs/toolkit react-redux

 

그리고 이전에 state를 호출하기 위한 getState와 action을 호출하기위한 dispatch를 사용하는 것 대신 'react-redux'의 useSelector와 useDispatch를 사용하면 된다. typescript의 type 미적용시 기본적인 사용법은 다음과 같다.

// src/features/icecream/IcecreamView.tsx
// type 미지정

import React from 'react';
import { useSelector, useDispatch } from 'react-redux'
import { ordered } from './icecreamSlice'

export const IcecreamView = () => {
  const numOfIcecream = useSelector(state => state.icecream.numOfIcecream
  const dispatch = useDispatch()
  return(
    <div>
      <h2> Num of icecream - {numOfIcecream} </h2>
      <button onClick={()=>dispatch(ordered())}>Order icecream</button>
    </div>
  )
}

typescript의 type을 지정해야한다면, store로부터 store states type과 dispatch type을 반환하도록 설정하고 이를 custom hook으로 custom useSlector와 useDispatch를 만들도록 한다.

그리고 위의 IcecreamView.tsx 에서 react-redux 의 useSelector, useDispatch를 사용하는 대신 customhook의 useAppSelector, useAppDispatch를 사용하도록 하면된다.

import { configureStore } from '@reduxjs/toolkit'
import cakeReducer from '../features/cake/cakeSlice'
import icecreamReducer from '../features/icecream/icecreamSlice'


const store = createStore({
  reducer:{
    cake:cakeReducer,
    icecream: icecreamReducer
  }
})

export default store
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
//src/app/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'

export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppDispatch= () => useDispatch<AppDispatch>()

 

728x90
//asyncActions.js

const { createStore, applyMiddleware } = require('redux');
const thunkMiddleware = require('redux-thunk').default;
const axios = require('axios');

const FETCH_USERS_REQUEST = 'FETCH_USERS_REQUEST';
const fetchUsersRequest = () => ({
  type: FETCH_USERS_REQUEST,
});

const FETCH_USERS_SUCCESS = 'FETCH_USERS_SUCCESS';
const fetchUsersSuccess = (users) => ({
  type: FETCH_USERS_SUCCESS,
  users,
});

const FETCH_USERS_ERROR = 'FETCH_USERS_ERROR';
const fetchUsersError = (error) => ({
  type: FETCH_USERS_ERROR,
  error,
});

const initialState = {
  loading: false,
  users: [],
  error: '',
};

const fetchUsers = () => {
  return function (dispatch) {
    dispatch(fetchUsersRequest());
    axios
      .get('https://jsonplaceholder.typicode.com/users')
      .then((res) => {
        const users = res.data.map((user) => user.id);
        dispatch(fetchUsersSuccess(users));
      })
      .catch((error) => {
        dispatch(fetchUsersError(error.message));
      });
  };
};

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case FETCH_USERS_REQUEST:
      return {
        ...state,
        loading: true,
        users: [],
        error: '',
      };
    case FETCH_USERS_SUCCESS:
      return {
        ...state,
        loading: false,
        users: action.users,
        error: '',
      };
    case FETCH_USERS_ERROR:
      return {
        ...state,
        loading: false,
        users: [],
        error: action.error,
      };
    default:
      return state;
  }
};

const store = createStore(reducer, applyMiddleware(thunkMiddleware));
console.log('intial state : ' + store.getState());
const unsubscribe = store.subscribe(() => {
  console.log(store.getState());
});

store.dispatch(fetchUsers());

기존 redux에서 async action처리를 위한 redux thunk middleware를 접목한 구현은 다음과 같다.

action을 직접 호출하는 대신 thunk를 호출하면 thunk 내에서 action을 호출하여 reducer에서 그에 맞는 state를 반영하는 개념이었다.

 

 

redux-toolkit도 크게 다르지 않다. redux와 크게 다르게 크게 명심해야할 점은 두가지가 있다.

  • thunk 내 action dispatch 불필요 및 action type 이름 자동 지정 (pending, fulfilled, rejected)
  • action에 대한 reducer를 extraReducers에 정의
// userSlice.js
const { createSlice } = require('@reduxjs/toolkit');
const { createAsyncThunk } = require('@reduxjs/toolkit');
const axios = require('axios');

//initial state
const initialState = {
  loading: false,
  users: [],
  error: '',
};

//thunk
const fetchUsers = createAsyncThunk('users/fetchUsers', () => {
  return axios.get('https://jsonplaceholder.typicode.com/users').then((res) => {
    return res.data.map((user) => user.id);
  });
});

//reducer
// redux thunk 내에서 지정된 type 이름의 action이 자동 dispatch =>  pending, fulfilled, rejected
const userSlice = createSlice({
  name: 'user',
  initialState,
  extraReducers: (builder) => {
    builder.addCase(fetchUsers.pending, (state) => {
      state.loading = true;
    });
    builder.addCase(fetchUsers.fulfilled, (state, action) => {
      state.loading = false;
      state.users = action.payload;
      state.error = '';
    });
    builder.addCase(fetchUsers.rejected, (state, action) => {
      state.loading = false;
      state.users = [];
      state.error = action.error.message;
    });
  },
});

module.exports = userSlice.reducer;
module.exports.fetchUsers = fetchUsers;
//index.js
const { configureStore } = require('@reduxjs/toolkit');
const userReducer = require('./userSlice');
const { fetchUsers } = require('./userSlice');

const store = configureStore({
  reducer: {
    user: userReducer,
  },
});

store.subscribe(() => {
  console.log(store.getState());
});

store.dispatch(fetchUsers());
728x90

redux와 redux toolkit은 얼핏보면 사용에 있어 다른 점이 없어보인다.

 

하지만 특정 module의 action이 호출 되었을때 다른 module의 state값을 바꿔야 할때  redux와 redux toolkit의 동작방식은 달라진다.

 

다음의 예시를 보자

CAKE_ORDERED action이 발생했을때 numOfIcecreams도 1씩 줄어들게 reducer를 수정하도록하자.

 

 기존 redux의 경우 아래 처럼 특정 store module의 initial state와 관련없는 state를 변화시켜도 문제 없이 작동한다.

...

const icecreamReducer = (state = icecreamInitialState, action) => {
  switch (action.type) {
    case 'ADD_ICECREAM':
      return {
        ...state,
        numOfIcecreams: state.numOfIcecreams - action.quantity,
      };
    case 'ADD_CAKE':
      return {
        ...state,
        numOfIcecreams: state.numOfIcecreams - 1,
      };
    default:
      return state;
  }
};

...


redux-toolkit의 경우 action을 현재 slice의 name + action name으로 자동 지정해주기 때문에 어떻게 만들어야할지 감이 오질 않는다.

위의 경우 ordered의 정확한 action이름은 `icecream/ordered` 가 된다.

 

위의 상황을 해결할 수 있는 것이 바로 extraReducers 이다.

//icecreamSlice.js

const { createSlice } = require('@reduxjs/toolkit');

const icecreamSlice = createSlice({
  name: 'icecream',
  initialState: {
    numOfIcecreams: 10,
  },
  reducers: {
    ordered: (state, action) => {
      state.numOfIcecreams -= action.payload;
    },
  },
  extraReducers: {
    ['cake/ordered']: (state) => {
      state.numOfIcecreams -= 1;
    },
  },
});

//export reducer && actions
module.exports = icecreamSlice.reducer;
module.exports.icecreamActions = icecreamSlice.actions;

위 처럼 'cake/ordered' action이 호출되었을 때 numOfIcecream도 1씩 줄어들게 설정할 수 있다

 

728x90

이전에서 보았던 redux는 기본설정(boilerplate)가 복잡하였다.

 

기존 redux에서 달라진 점은 다음과 같다.

  • intialState와 reducer를 createSlice 내에서 묶어서 관리
  • action,action creator가 reducer내에서 자동설정
  • nested state object에 대해 immer package 없이 간편하게 처리
  • combineReducer 함수 import 불필요

 

redux-toolkit에 추가되는 함수는 다음과 같다.

  • createSlice : action, action creator, initialstate, reducer 관리
  • configureStore : 기존 createStore 대체

위의 사항들을 기반으로 구현된 기본 redux toolkit 구성은 다음과 같다.

 

// cakeSlice.js
const { createSlice } = require('@reduxjs/toolkit');

const cakeSlice = createSlice({
  name: 'cake',
  initialState: {
    nested: {
      numOfCakes: 10,
    },
  },
  reducers: {
    ordered: (state) => {
      state.nested.numOfCakes--;
    },
  },
});

// export reducer && actions
module.exports = cakeSlice.reducer;
module.exports.cakeActions = cakeSlice.actions;
//icecreamSlice.js
const { createSlice } = require('@reduxjs/toolkit');

const icecreamSlice = createSlice({
  name: 'icecream',
  initialState: {
    numOfIcecreams: 10,
  },
  reducers: {
    ordered: (state, action) => {
      state.numOfIcecreams -= action.payload;
    },
  },
});

//export reducer && actions
module.exports = icecreamSlice.reducer;
module.exports.icecreamActions = icecreamSlice.actions;
//index.js
const { configureStore } = require('@reduxjs/toolkit');
const cakeReducer = require('./cakeSlice');
const icecreamReducer = require('./icecreamSlice');
const cakeActions = cakeReducer.cakeActions;
const icecreamActions = icecreamReducer.icecreamActions;

const store = configureStore({
  reducer: {
    cake: cakeReducer,
    icecream: icecreamReducer,
  },
});

console.log('initialState: ', store.getState());

const unsubscribe = store.subscribe(() => {
  console.log(store.getState());
});

store.dispatch(cakeActions.ordered());
store.dispatch(icecreamActions.ordered(3));

unsubscribe();

728x90

redux의 구성

  • Action : string
    • action type 지정
  • Action Creator : function(return object)
    • Action Type과 payload 를 반환
  • Reducer : function(return state mutation)
    • state(initialstate) 와 action은 parameter로 받음
  • Store
    • createStore 로 생성 
    • reducer 와 middleware를 매개 변수로 받음
  • Middleware
    • action과 reducer 사이에 중간 처리해주는 함수
    • logger, thunk등

 

다음은 위의 redux 기본구조를 구현한 모습니다. (middleware는 생략)

 

const { createStore, combineReducers } = require('redux');
const produce = require('immer').produce;

//action && action creator
const ADD_CAKE = 'ADD_CAKE';
const orderCake = () => ({
  type: ADD_CAKE,
  quantity: 1,
});

const ADD_ICECREAM = 'ADD_ICECREAM';
const orderIceCream = (q) => ({
  type: ADD_ICECREAM,
  quantity: q,
});

//initialState
const cakeInitialState = {
  //immer 시현용으로 일부러 nested
  nested: {
    numOfCakes: 10,
  },
};

const icecreamInitialState = {
  numOfIcecreams: 10,
};

//reducer
const cakeReducer = (state = cakeInitialState, action) => {
  switch (action.type) {
    case 'ADD_CAKE':
      //굳이 produce 쓸필요 없지만 nested object case 라 가정
      return produce(state, (draft) => {
        draft.nested.numOfCakes -= action.quantity;
      });
    default:
      return state;
  }
};

const icecreamReducer = (state = icecreamInitialState, action) => {
  switch (action.type) {
    case 'ADD_ICECREAM':
      return {
        ...state,
        numOfIcecreams: state.numOfIcecreams - action.quantity,
      };
    default:
      return state;
  }
};

//store && logging
const store = createStore(combineReducers({ cake: cakeReducer, icecream: icecreamReducer }));
console.log('initial state', store.getState());

const unsubscribe = store.subscribe(() => {
  console.log(store.getState());
});

store.dispatch(orderCake());
store.dispatch(orderIceCream(2));

unsubscribe();

728x90
//package.json
{
  "name": "polz-blog",
  "private": true,
  "version": "0.0.0",
  "scripts": {
    ...,
    "lint": "npx eslint \"**/*.{ts,tsx}\"",
    "lint:fix": "eslint --fix \"**/*.{ts,tsx}\"",
    "lint-staged": "lint-staged",
    "prepare": "husky install"
  },
  "dependencies": {
    ...
  },
  "devDependencies": {
    ...
    "@typescript-eslint/eslint-plugin": "^5.19.0",
    "@typescript-eslint/parser": "^5.19.0",
    "eslint": "^8.2.0",
    "eslint-config-airbnb": "^19.0.4",
    "eslint-config-prettier": "^8.5.0",
    "eslint-import-resolver-typescript": "^2.7.1",
    "eslint-plugin-import": "^2.25.3",
    "eslint-plugin-jsx-a11y": "^6.5.1",
    "eslint-plugin-prettier": "^4.0.0",
    "eslint-plugin-react": "^7.28.0",
    "eslint-plugin-react-hooks": "^4.3.0",
    "husky": "^7.0.0",
    "lint-staged": "^12.3.7",
    "prettier": "^2.6.2",
  	...
  },
  "lint-staged": {
    "*.{ts,tsx,js,jsx}": [
      "npm run lint:fix"
    ]
  }
}

//.husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npm run lint-staged

husky init

husky install

//tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "allowJs": false,
    "skipLibCheck": false,
    "esModuleInterop": false,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"], // 사용할 경로
    },
  },
  "include": ["src/**/*.ts", "src/**/*.tsx"],
  "references": [{ "path": "./tsconfig.node.json" }]
}
//.eslintrc.js
module.exports = {
    "env": {
        "browser": true,
        "es2021": true
    },
    "extends": [
        "plugin:react/recommended",
        "airbnb",
        'prettier'
    ],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": "latest",
        "sourceType": "module"
    },
    "plugins": [
        "react",
        "@typescript-eslint",
        "prettier"
    ],
    "rules": {
        /*tsx에서 jsx 문법을 가능하게 해준다*/
        "react/jsx-filename-extension":["warn",{"extensions":[".tsx"]} ],
        /*ts tsx 확장자를 안써도 되게해준다. 해당파일이름의 ts|tsx파일을 찾아 자동 import한다*/
        "import/extensions":[
            "error",
            "ignorePackages",
            {
              "ts":"never",
              "tsx":"never"
            }
        ],
         'prettier/prettier': 'error',
    },
    /*tsconfig 설정을 가져온다*/
    "settings":{
        "import/resolver":{
          "typescript":{}
        }
      }
}

'React' 카테고리의 다른 글

Redux toolkit - extraReducers(2) - redux thunk  (0) 2022.05.15
Redux toolkit - extraReducers(1) - module  (0) 2022.05.15
Redux toolkit - Redux toolkit 기본  (0) 2022.05.15
Redux toolkit - redux 기본  (0) 2022.05.15
React 18 변경 사항  (0) 2022.04.11
728x90

 

17에 비해 코드상으로 수정해야할 부분이 많지는 않다는 것이 다행이다. React18은 React 17에서 어떤점이 바꼈는지 알아보자

    1.  html과 js를 연결해주는 main.js안에서 사용하는 함수가 바꼈다.
      import ReactDOM from 'react-dom'
      import App from './App'
      
      //17
      //ReactDOM.render(<App />, document.getElementById('root'));
      
      //18
      const root = ReactDOM.createRoot(document.getElementById('root'));
      root.render(<App />);
    2. Concurrent Rendering
      동시에 일어난 다수의 상태 업데이트 처리하는 기법으로 urgent한 update는 오래 걸리거나 급박하지않은 update보다 우선되어진다.
      만약 엄청나게 많은 component가 동시에 변해야하는 상황이라면 
      React 17에서는 동시에 state 변화가 일어난다고 해도 먼저 일어난 것 순서대로 순차적으로 처리되는 반면
      React 18에서는 동시에 state 변화가 일어나야할 시 우선도를 보고 urgent한 update가 우선되어지고 urgent하지 않거나 오래걸리는 update는 background에서 수행된다.

      우선도 구분은 다음 API를 사용하여 우선도가 높은 상태변화와 낮은 상태변화를 구분한다. 
      - useTransition( )
      - startTransition( )
      - useDeferredValue( )

      useTransition startTransition useDefferedValue
      React에게 lower priority의  state update를 알려준다 - React에게 갱신된 값이 준비될때까지 이전값을 UI상 display 해야한다는 것을 알려준다
      - 2개의 value 중 하나는 업데이트가 빠르고 나머지 하나는 업데이트가 느린경우 느린 업데이트의 value에 대해서는 update전 값을 보여준다.
      함수형 컴포넌트에서 사용한다 hook이 사용될 수 없는 환경에서 사용한다
      [isPending, startTransition] 반환
      //isPending은 현재 background에서 state가 변화하고 있음을 알려준다
       
      startTransition(()=>setUser(user)); const deferredVal=useDeferredValue(value);
      import { useTransition } from 'react';
          
      const [isPending, startTransition] = useTransition();
      // Urgent
      setInputValue(input);
      
      // Mark any state updates inside as transitions
      startTransition(() => {
        // Transition
        setSearchQuery(input);
      })
      {isPending && <Spinner />}
      import { useState, useDeferredValue } from "react";
          
      function App() {
        const [input, setInput] = useState("");
        const deferredValue = useDeferredValue(text, { timeoutMs: 3000 }); 
      
        return (
          <div>
            <input value={input} onChange={handleChange} />
            <MyList text={deferredValue} />
          </div>
        );
       }
    3. State Batching 개선
      State Batching: 다수의 state 업데이트가 함께 실행된다. 그러므로 컴포넌트가 한번만 업데이트된다.
      function increaseCouterHandler() {
        //Two state update calls batched together
        // +3 씩 이뤄진다
        setCounter((curCounter)=>curCounter+1);
        setCounter((curCounter)=>curCounter+2);
      }
      
      function increaseCouterAsyncHandler() {
        setTimeout(()=>{
          //Two state update calls not batched when using react 17
          //+3 씩 이뤄지는데 오직 asynchronous 환경에서만 이뤄졌다
          setCounter((curCounter)=>curCounter+1);
          setCounter((curCounter)=>curCounter+2);
        })
      }
    4. Suspense 개선
      Suspense: Code(or data) fetching과 관련된 UI updates를 도와주는 component. 주로 lazy loading할때 사용한다.
      React 18에서는 Server side rendering 환경에서도 Suspense를 사용할 수 있다.

+ Recent posts