[TIL 2022.12.31] Redux, Redux-Toolkit, Axios 개념 복습

김헤일리·2022년 12월 30일
0

TIL

목록 보기
17/46

벌써 항해한지 꽤 오랜 시간이 지났다. 리액트 주차를 지나면서 배운 것은 Redux, Redux-Toolkit, Axios였는데, 미니 프로젝트와 클론코딩 프로젝트를 진행하면서 열심히도 써먹었었다.

근데 다만 정말 말 그대로 써먹기만 했지, 뭔가 계속 개념이 헷갈리는 것 같아서 다시 한번 정리하려고 한다.


1. Redux

1. Redux의 전반적인 설명

공식 문서에 따르면 Redux는 자바스크립트 앱을 위한 예측 가능한 상태 컨테이너라고 한다.
리덕스를 사용하는 가장 큰 이유는 리액트에서 컴포넌트가 많아질수록 구조가 복잡해져서 상태 변경을 하기 어려워지기 때문이다. 리덕스를 사용할 경우 데이터가 Store에 저장되기 때문에 props drilling이 발생하지 않는다.

리덕스를 이해하기 위해 데이터 흐름을 한번 그려보았다.

간단하게 정리하자면:

  1. 사용자의 행동은 Action으로서 state에 변화를 준다.
  2. 발생한 Action은 Dispatcher라는 캐리어가 리덕스 스토어에 전달한다.
  3. Dispatcher가 보낸 Action은 Reducer라는 함수에 도착하고, Reducer는 Action
  4. state를 저장하고 있는 Store는 Reducer로 인해 State를 변경시킨다.
  5. State가 변경되면 렌더링이 발생하고 사용자에게 변경된 상태가 표시된다.

❗️ 리덕스를 사용할 때 숙지해야할 3가지 규칙

  1. 하나의 애플리케이션 안에는 하나의 스토어를 가져야 한다.
    • 하나의 App에는 하나의 스토어를 만들어 사용한다. 여러개의 스토어를 생성할 순 있지만, 권장하지 않는다고 한다.
    • 스토어를 여러개 두는 건 특정 업데이트가 너무 빈번하게 일어나거나 App의 특정 부분을 완전히 분리하게될 경우라고 한다. 하지만 스토어가 여러개가 된다면 리덕스에서 제공하는 개발 도구를 활용할 수 없다고 한다.
  1. 리덕스의 상태는 읽기 전용이다.

    • 리액트에서 state를 업데이트할 때 직접적으로 변경하지 않고 setState를 활용해서 불변성을 지키려고 하는 것처럼 리덕스도 기존의 상태를 수정하지 않고 새로운 상태를 생성하면서 업데이트를 한다. (교체하듯이)
    • Redux는 Shallow equality 검사로 상태의 변화를 감지하기 때문에 불변성을 유지해야한다. 불변성을 지키지 않으면 참조하는 주소가 변하지 않기 때문에 리덕스는 상태 변경을 감지하지 못 하고 state를 변경해도 렌더링이 발생하지 않게된다.
    • 또한 얕은 비교가 더 빠른 성능을 낼 수 있기 때문에 기존 상태의 객체를 새로운 객체로 변경하여 뱉어내면 얕은 비교를 통해 변화를 감지하는 것이 가능하다.
  2. 리듀서는 순수한 함수여야 한다.

    • 리듀서는 이전 상태와 액션 객체를 파라미터로 받는다.
    • 리듀서는 이전의 상태는 건들이지 않고 변화로 새로운 상태 객체를 뱉어내기 때문에 동일 인풋에 대한 동일 아웃풋이 보장되어야 한다. (순수해야 한다.)
    • 그렇지 않은 경우엔 리덕스 미들웨어라는 것을 사용한다.

2. Redux 사용법

1. 먼저 리덕스에서 관리할 상태를 정의한다.

const initialState = {
  counter: 0,
  text: '',
  list: []
};
  • 리덕스는 상태 관리를 하는 일종의 도구이기 때문에, 리덕스에서 관리할 상태는 보통 initialState로 정의해서 관리한다.

2. action type을 정의한다.

const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
const CHANGE_TEXT = 'CHANGE_TEXT';
const ADD_TO_LIST = 'ADD_TO_LIST';
  • action type은 주로 대문자로 정의하고, 고유한 이름을 가져야한다.
  • initialState를 변경시킬 수 있는 일종의 이벤트라고 생각할 수 있겠다.
  • action이 발생하면 해당 action type에 맞는 액션 생성 함수가 실행된다.

3. action creator 함수를 정의한다.

export function increase() {
  return {
    type: INCREASE
  };
}

export const decrease = () => ({
  type: DECREASE
});
  • 함수의 파라미터를 받아와서 해당하는 액션의 객체를 만드는 함수이다.
  • 액션 객체는 정의했던 action type을 필수로 갖고 있어야하고, 외부에서 사용하기 위해 export 해야한다.
  • 액션 생성 함수는 함수이기 때문에 화살표 함수로도 정의할 수 있다.
  • 액션 생성 함수를 만드는 이유는 매번 액션 객체를 새로 만들지 않기 위해서다.

4. 리듀서를 생성한다.

function reducer(state = initialState, action) {
  switch (action.type) {
    case INCREASE:
      return {
        ...state,
        counter: state.counter + 1
      };
    case DECREASE:
      return {
        ...state,
        counter: state.counter - 1
      };
    case CHANGE_TEXT:
      return {
        ...state,
        text: action.text
      };
    case ADD_TO_LIST:
      return {
        ...state,
        list: state.list.concat(action.item)
      };
    default:
      return state;
  }
  • 리듀서 함수의 매개변수는 위에 정의했던 initialState와 action이다.
  • 리듀서는 액션 생성함수를 통해 만들어진 액션 객체를 참조해서 state를 변경한다.
  • 액션 생성함수의 type에 따라 switch문에 걸리게되고, 해당하는 부분이 실행된다.
  • 리듀서 함수는 불변성을 유지하며 state를 변경시켜야한다.

5. store를 생성한다.

const store = createStore(reducer);

console.log(store.getState());
  • store 생성 함수를 사용해서 스토어를 생성하는데, 이때 매개변수는 리듀서 함수가 된다.
  • store.getState()를 사용하면 현재 store 안에 들어있는 상태를 조회할 수 있다.

6. Dispatcher를 이용해서 액션을 발생시킨다.

store.dispatch(increase());
store.dispatch(decrease());
store.dispatch(changeText('안녕하세요'));
store.dispatch(addToList({ id: 1, text: '와우' }));
  • 외부 컴포넌트에서 리덕스 스토어에 있는 initialState를 변경 시키기 위해 dispatcher를 사용한다.
  • 디스패치는 스토어의 내장 함수고, dispatch의 매개변수는 액션 생성 함수다.
  • 액션을 발생 시키는 또 다른 함수이고, 해당 함수가 액션을 리듀서로, 리듀서는 해당 액션을 스토어에 있는 state를 변경 시키는 것에 사용하는 것이다.

2. Redux와 Redux-Toolkit

그렇다면 리덕스와 리덕스 툴킷의 다른 점은 뭘까?

공식 문서에 따르면 Redux 로직을 작성하기 위해 공식적으로 Redux-Toolkit 사용을 추천한다고한다.
RTK(Redux-Toolkit)는 Redux 앱을 만들기에 필수적으로 여기는 패키지와 함수들이 이미 포함되어있기 때문에 훨씬 더 효율적이다.

RTK는 저장소 준비, 리듀서 생산과 불변 수정 로직 작성, 상태 "조각" 전부를 한번에 작성 등 일반적인 작업들이 단순화 됐기 때문에 효율적이다.

1. 저장소 준비 ( configureStore )

// 리듀서가 1개일 때
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'

const store = configureStore({ reducer: rootReducer })

// 리듀서가 여러개일 때
import { combineReducers } from 'redux'
import todos from './todos'
import counter from './counter'

export default combineReducers({
  todos,
  counter
})
  • configureStore를 사용할 경우, 여러개의 reducer를 담아도 combineReducers를 통해 rootReducer로 묶을 수 있다.

2. 리듀서 생산과 불변 수정 로직 작성 ( createReducer() )

import { createAction, createReducer } from '@reduxjs/toolkit'

const increment = createAction('counter/increment')
const decrement = createAction('counter/decrement')
const incrementByAmount = createAction('counter/incrementByAmount')

const initialState = { value: 0 }

const counterReducer = createReducer(initialState, (builder) => {
  builder
    .addCase(increment, (state, action) => {
      state.value++
    })
    .addCase(decrement, (state, action) => {
      state.value--
    })
    .addCase(incrementByAmount, (state, action) => {
      state.value += action.payload
    })
})
  • 기존의 switch문을 사용하지 않아도 바로 Action을 정의하고, 해당 액션에 따른 리듀서 함수를 바로 정의할 수 있다.
  • 해당 함수엔 불변성을 지키는 immer 라이브러리가 내장되어있기 때문에 전개 연산자를 활용하지 않아도 상태값이 불변 업데이트된다고 한다.

3. 상태 "조각" 한번에 작성 ( createSlice )

import { createSlice } from '@reduxjs/toolkit'

const initialState = { value: 0 }

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment(state) {
      state.value++
    },
    decrement(state) {
      state.value--
    },
    incrementByAmount(state, action) {
      state.value += action.payload
    },
  },
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
  • initialState와, 액션객체, 리듀서 함수까지 한번에 작성이 가능하다.
  • createReducer() 함수조차 사용하지 않아도 되기 때문에 훨씬 더 짧은 코드를 작성할 수 있다.
  • 또한 extraReducer라는 외부 액션을 참조할 수 있는 또 다른 형식의 리듀서가 있는데, 해당 리듀서는 createAsyncThunk()와 같은 비동기 함수와 함께 사용할 수 있다.
  • Thunk를 사용하면, 객체가 아닌 함수를 dispatch 할 수 있게 해준다.
  • createAsyncThunk() 의 첫번째 자리에는 action value, 두번째에는 함수가 들어간다.

나는 주로 RTK와 thunk를 함께 조합해서 사용했고, 다른 방식은 크게 생각해보지도 않았기 때문에 굳이 전역상태 관리가 필요하지 않은 케이스에도 리덕스를 사용한것 같다.

리덕스 공식문서에도 정말 리덕스가 필요한지 생각하고 사용하라는 말이 있던데, 전역 상태관리가 정말 필요한 순간이 있고 편하게 사용할 순 있지만, 하나의 함수를 만들기 위해서 부가적으로 작성하는 코드가 너무 많은 것은 확실히 비효율적인 것 같다. 아무리 RTK를 쓴다고해도 말이지!

다음번엔 리덕스를 사용하지 않고 CRUD를 구현하도록 해야겠다.



2. Axios

1. Axios의 전반적인 설명

공식 문서에 따르면 Axios는 node.js와 브라우저를 위한 Promise 기반 HTTP 클라이언트라고 한다.
Http 메소드를 활용해서 서버와 통신할 때 사용하는 패키지라고 이해할 수 있다.

내장되어있는 기능인 fetch와는 다르게 모듈을 따로 설치해야하지만, fetch보다 호환성이 좋고 기능이 더 다양하기 때문에 더 많이 사용된다.

Axios의 특징:

  1. 브라우저를 위해 XMLHttpRequests 생성
  2. node.js를 위해 http 요청 생성
  3. Promise API를 지원
  4. 요청 및 응답 인터셉트
  5. 요청 및 응답 데이터 변환
  6. 요청 취소
  7. JSON 데이터 자동 변환
  8. XSRF를 막기위한 클라이언트 사이드 지원

2. Axios 사용법

Axios는 기본적으로 HTTP Method와 요청 URL을 넣어서 config를 전송하면 서버에 요청을 보낼 수 있다.
하지만 매번 요청 URL을 작성할 필요 없이 baseURL을 지정할 수 있는데, 이때 만들게 되는 것은 Axios Instance다.

1. Axios Instance

const instance = axios.create({
  baseURL: 'https://some-domain.com/api/',
  timeout: 1000,
  headers: {'X-Custom-Header': 'foobar'}
});
  • 이때 필수적으로 필요한건 baseURL이다.
  • 인스턴스를 생성할 경우, 요청을 보낼 때 baseURL 부분은 생략이 가능하다.

2. Axios 요청 Config

  • 요청을 보낼 때 사용하는 config 중 오직 URL만 필수이고, method는 따로 지정하지 않을 경우 자동적으로 GET 이 선택된다.
  • 요청 Config는 종류가 많기 때문에, 필요할 때 마다 참고해야겠다.

3. Axios Interceptor

// 요청 인터셉터
axios.interceptors.request.use(function (config) {
    return config;
  }, function (error) {
    return Promise.reject(error);
  });

// 응답 인터셉터
axios.interceptors.response.use(function (response) {
    return response;
  }, function (error) {
    return Promise.reject(error);
  });
  • axios는 요청이나 응답이 실행되기 전에 중간에 가로채서 필요한 로직을 추가할 수 있다.
  • 보통 요청 인터셉터의 경우, 요청을 보내기 전에 header에 access token을 담아서 요청을 보내기 위해서 사용했고, 응답 인터셉터의 경우, 로그인이 만료되었을 때 에러를 내뱉지 않고 사용자에게 만료를 정상적으로 안내 하기 위해서 사용했었다.

4. Axios 요청 취소

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // 에러 핸들링
  }
});

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

source.cancel('Operation canceled by the user.');
  • axios는 취소 토큰을 이용해 요청을 취소할 수 있다.
  • CancelToken.source 팩토리를 사용하여 취소 토큰을 만들거나, 실행자 함수를 CancelToken 생성자에 전달하여, 취소 토큰을 만들 수 있다.
  • 직접 사용해본적은 없지만, 보통 요청이 너무 많이갈 경우 사용한다고 한다.

3. Axios와 Fetch

Fetch는 JavaScript 브라우저에 내장되어있는 라이브러리로, 서버에 네트워크 요청을 보내고 새로운 정보를 받아오기 위해 사용한다. fetch()를 호출하면 브라우저는 네트워크 요청을 보내고 프라미스가 반환되는데, fetch 응답은 대부분 두 단계를 거쳐서 진행된다.

  1. 서버에서 응답 헤더를 받자마자 fetch 호출 시 반환받은 promise가 내장 클래스 Response의 인스턴스와 함께 이행된다.
  2. 추가 메서드를 호출해 응답 본문을 받는다. response에는 프로미스를 기반으로 하는 다양한 메서드가 있기 때문에, 이 메서드들을 사용해서 다양한 형태의 응답 본문을 처리한다.

Fetch는 따로 모듈을 설치할 필요가 없어서 편리하지만, axios에 비해 기능이 적어서 덜 사용하는 추세라고한다.

Axios vs Fetch 비교 차트:

Fetch를 직접 사용해보진 않았지만, JSON 변환도 자동으로 안되고 인트턴스와 인터셉터도 만들 수 없으니 앞으로도 웬만해선 사용하지 않을 것 같다. 그래도 내장 라이브러리인 만큼 어떻게 동작하는지 나중에 더 찾아보면 좋을 것 같다.



오늘은 지금까지 사용은 했지만 개념적으로는 아직 친숙하지 않은 라이브러리들을 한번 살펴보았다.
정리는 매우 오래 걸렸고... 아직 당연히 전부 알지 못 하는 것 같아서 앞으로 더 공부해야겠지만, 적어도 그냥 쓰지 않고 앞으로는 최소한의 장단점은 알고 사용할 수 있을 것 같다.

코딩할때 그냥 사용하지말고 왜 이 기술, 왜 이런 방식을 사용하는지 아는 것은 중요하다고 했다.
지금까진 항해에서 배우면서 이것밖에 몰라서 사용했지만, 적어도 내가 유일하게 아는 방식들에 대해서 더 자세히 알아보도록 노력해야겠다.

출처:

profile
공부하느라 녹는 중... 밖에 안 나가서 버섯 피는 중... 🍄

0개의 댓글