프로젝트/날씨 칵테일

[프로젝트] 날씨 칵테일 | 1. reducer, dispatch, action을 이용한 Redux-Toolkit state 동작원리

paintover23 2023. 10. 12. 15:14
728x90

[Redux 개요]

Redux는 Action, Store, Reducer, Dispatch를 통해 상태를 관리한다. 각 역할은 아래와 같다.

- Store : 상태를 저장하는 장소
- Reducer : 현재 Store의 상태와, 발생한 Action의 payload를 받아 새로운 상태를 반환 (상태가 어떤식으로 변경되는 지를 정의함)
- Dispatch : Action을 발생시킴
- Action : 상태를 변경시키고 싶을 때 생성하는 객체(type 과 payload를 가진다)

 

[state 동작원리]

이제 state가 어떤 순서를 거쳐 갱신되는 지 그 여정을 되짚어 보자. 아래 순서는 store 구축 순서를 포괄하니 코드 입력 시에도 동일한 순서로 진행하면 된다.

 

1. slice 구성하기

// weatherSlice.ts

// initialState의 type
const initialState: WeatherState = {
  weatherInfo: null,
};

// slice 만들기
const weatherSlice = createSlice({
  name: 'weather',
  initialState,
  reducers: {
    setWeatherInfo: (state, action: PayloadAction<WeatherType>) => {
      state.weatherInfo = action.payload;
    },
  },
});

// actions 및 reducer 추출
export const { setWeatherInfo } = weatherSlice.actions;
export default weatherSlice.reducer;
// weatherSlice.interface.ts

// weather state 세부 요소들을 weatherInfo 라는 한 개 단위로 묶음
export interface WeatherState {
  weatherInfo: WeatherType | null; // weatherType 또는 null 형식
}

export interface WeatherType {
  name: string; // 지역명
  main: {
    temp: number;
  };
  weather: {
    main: any; // 날씨(영문)
    description: string; // 날씨(국문)
    icon: string; // 날씨 아이콘
  }[];
}

(1) slice는 일종의 작은 store이다. slice는 이름(name), 초기 상태값(initialState), 그리고 reducers(액션의 payload를 받아 새로운 상태로 변경하는 함수)의 세요소로 구성되어 있다.

 

(2) intialState의 type을 지정해 줄 수 있다. 초반값은 null로 지정해 주었다. 또한 weatherState의 세부요소들은 weatherInfo라는 한 개 단위로 묶어서 표현하였다.

 

(3) reducers는 여러 단일 리듀서 함수를 연달아 작성할 수 있다. 코드에서는 setWeatherInfo 함수 하나만 사용하였다. 인자로는 state와 action을 받는데, 이 함수의 역할은 액션의 역할을 하며 동시에 state의 변화까지 구현하는 것이다. immer.js를 내장하고 있기에 state 값을 따로 return 하지 않아도 된다. dispatch에 의해 액션이 발생할 때마다 state는 action의 payload 값으로 갱신된다.

 

(4) 전술한 바와 같이 액션은 type과 payload로 구성되어 있다.  코드에서 PayloadAction<T>는 특정 payload 타입을 가진 액션 객체를 설명한다. 즉, 코드에서 action 의 payload는 "WeatherType" 이라는 type을 띄고 있다.

 

(5) store에 등록하기 위해 actions와 reducers를 export 한다.

 

2. slice를 store에 등록하기

// store.ts

import { configureStore } from '@reduxjs/toolkit';
import coordinatesReducer from '../features/coordinatesSlice';
import isCheckedReducer from '../features/isCheckedSlice';
import searchResultsReducer from '../features/searchResultsSlice';
import weatherReducer from '../features/weatherSlice';
import cocktailReducer from '../features/cocktailSlice';

const store = configureStore({
  reducer: {
    coordinates: coordinatesReducer,
    isChecked: isCheckedReducer,
    searchResults: searchResultsReducer,

    weather: weatherReducer,
    cocktail: cocktailReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export { store };

(1) export default 한 weatherSlice.reducer는 weatherReducer라는 이름으로 바꿔서 store에 등록시키는 것이 일반적이다.

 

(2) state와 dispatch에 대해 각각 RooteState와 AppDispatch 라는 타입을 설정하고 다른 곳에서 쓸 수 있도록 export 한다. 이를 통해 state와 dispatch에 접근시 타입 안정성을 보장받을 수 있다. 

 

(3) store를 export한다.

 

 

3. useAppDispatch, useAppSelector hooks 만들기

// hooks.ts

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

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

(1) store 내의 dispatch와 state를 사용하기 위해 useDispatch와 useSelector를 사용하는 것이 일반적이다. 그러나 타입스크립트가 적용된 경우 매번 타입 선언을 해주는 것이 번거롭기 때문에, 공식문서에서도 별도의 hooks를 지정하는 것을 권장하고 있다. 이를 통해 useSelector의 경우, 매번 (state: RootState)를 타이핑하는 수고를 덜어준다.

 

(2) hooks을 사용하지 않는다면 다음과 같이 작성할 수 있다:

     (hook 사용o) const weatherInfo = useAppSelector((state) => state.weather.weatherInfo);

     (hook 사용x) const weatherInfo = useSelector((state:RootState) => state.weather.weatherInfo);

 

4. component 에서 state와 dispatch 사용하기

// GetWeather.tsx

import { useEffect } from 'react';
import { fetchWeatherData, getLocation } from '../../../API/WeatherAPI/index';
import { useDispatch } from 'react-redux';
import { setWeatherInfo } from '../../../features/weatherSlice';
import GetCocktail from '../getCocktail/GetCocktail';
import { useAppSelector } from '../../../app/hooks';

function GetWeather() {
  const dispatch = useDispatch();

  // action dispatch 하기
  useEffect(() => {
    const getCurrentWeather = async () => {
      try {
        const position = await getLocation();
        const { latitude, longitude } = position.coords;
        const weatherData = await fetchWeatherData(latitude, longitude);
        // dispatch(action(payload))
        dispatch(setWeatherInfo(weatherData)); // setWeatherInfo(액션명), weatherData(페이로드)
      } catch (error) {
        console.error('Error:', error);
      }
    };
    getCurrentWeather();
  }, [dispatch]);

  // state에 저장된 값 weatherInfo에 불러오기
  const weatherInfo = useAppSelector((state) => state.weather.weatherInfo);
  // 중략...

  // 화면에 표시하는 UI
  const { name, main, weather: weatherDetails } = weatherInfo;
  
  // GetCocktail 컴포넌트에 내려 줄 props 만들기
  const weatherName = weatherDetails[0].main; // 날씨명(영문)

  return (
    // 중략...
      <GetCocktail weatherName={weatherName}></GetCocktail>
  );
}

export default GetWeather;

(1) dispatch는 action을 인자로 받고 action은 payload를 인자로 받는다. dispatch가 발동하면 slice에서 정의한 reducers를 호출하여 state를 갱신하게 된다. 코드에서 밑줄친 weatherData가 payload임을 증명하는 것은 그것의 type이 slice에서 정의한 PayloadAction<WeatherType> 형식을 정확히 따르고 있기 때문이다.

 

(참고) React DevTools 다운로드

- 리덕스 상태 관리를 시각화 해서 보여주는 tool이 있다. 아래 링크에서 다운로드 받을 수 있다: https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ko

step1 : init
step 2: weather(슬라이스명)/setWeatherInfo(액션)
step 3: cocktail(슬라이스명)/fetchCocktail(액션)- pending(대기중)
step 4: cocktail(슬라이스명)/fetchCocktail(액션)- fulfilled(완료)

 

 

- 참고자료:

https://react-redux.js.org/using-react-redux/usage-with-typescript

https://velog.io/@gsh723/%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC-Redux-Toolkit-%EC%9D%B4%EB%9E%80

https://moon-ga.github.io/react_redux/3-using-react-redux-with-redux-toolkit/

728x90
반응형