231218 TIL (2)

thisisyjin·2023년 12월 18일
0

TIL 📝

목록 보기
102/113

Redux

  • JavaScript를 위한 상태관리 라이브러리.

Middleware 없이 카운터 제작

프로젝트 생성

$ npx create-react-app react-redux --template typescript
$ npm install redux --save

App.tsx 수정

import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      Clicked: times
      <button>+</button>
      <button>-</button>
    </div>
  );
}

export default App;

리듀서 작성

src/reducers/index.tsx 생성

const counter = (state = 0, action: { type: string }) => {
    switch (action.type) {
        case "INCREMENT":
            return state + 1;
        case "DECREMENT":
            return state - 1;
        default:
            break;
    }
}

export default counter;

CreateStore

  • 앱의 전체 상태 트리를 보유하는 Redux 스토어 생성
  • 하나의 앱에는 하나의 스토어만 존재해야 함.

store.getState()

  • 애플리케이션의 현재 상태 트리를 반환합니다. 스토어의 리듀서가 마지막으로 반환한 값과 동일합니다.
// index.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import counter from './reducers';
import { createStore } from 'redux';

const store = createStore(counter);

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <App
      value={store.getState()}
      onIncrement={() => store.dispatch({ type: 'INCREMENT' })}
      onDecrement={() => store.dispatch({ type: 'DECREMENT' })}
    />
  </React.StrictMode>
);

App 컴포넌트의 props로 해당 값들을 보내줌.

// App.tsx

import React from 'react';
import logo from './logo.svg';
import './App.css';

type Props = {
  value: number;
  onIncrement: () => void;
  onDecrement: () => void;
};

function App({ value, onIncrement, onDecrement }: Props) {
  return (
    <div className="App">
      Clicked: {value} times
      <button onClick={onIncrement}>+</button>
      <button onClick={onDecrement}>-</button>
    </div>
  );
}

export default App;
  • 버튼 클릭 시, 해당 액션이 디스패치됨.
  • 액션이 디스패치 되면, 리듀서 함수에 의해 action.type 에 맞게 state를 리턴함.

Subscribe

  • store.subscribe()
  • render 함수를 선언하고, subscribe 함수의 params로 넣어줌.
// App.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import counter from './reducers';
import { createStore } from 'redux';

const store = createStore(counter);

const render = () => {
  const root = ReactDOM.createRoot(
    document.getElementById('root') as HTMLElement
  );
  root.render(
    <React.StrictMode>
      <App
        value={store.getState()}
        onIncrement={() => store.dispatch({ type: 'INCREMENT' })}
        onDecrement={() => store.dispatch({ type: 'DECREMENT' })}
      />
    </React.StrictMode>
  );
};

render();
store.subscribe(render);

combineReducer

  • 하나의 리듀서(Root Reducer)을 생성해야 함.
  • 여러개이 리듀서가 있는 경우, 하나의 리듀서로 합쳐주기 위해 combineReducer 메서드 사용.
  • 리듀서 함수를 하나 더 생성해보자.
  • Todo를 추가하는 todos 리듀서 함수.
// reducers/todos.tsx
enum ActionType {
  ADD_TODO = 'ADD_TODO',
  DELETE_TODO = 'DELETE_TODO',
}

interface Action {
  type: ActionType;
  text: string;
}

const todos = (state = [], action: Action) => {
  // Action.text 값을 받아옴 (Payload)
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, action.text];
    default:
      return state;
  }
};

export default todos;

[참고] TypeScript의 enum

  • Union(|)을 이용해도 좋음.
  • enum을 사용해 리터럴의 타입과 값에 이름을 붙여서 코드의 가독성을 크게 높일 수 있음.
  • enum은 객체이다. (단, 객체와 달리 enum의 속성은 변경할 수 없음.)
// reducers/index.tsx
import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';

const rootReducer = combineReducers({
  counter,
  todos,
});

export default rootReducer;

createStore에 루트 리듀서 대체

// index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { createStore } from 'redux';
import rootReducer from './reducers';

const store = createStore(rootReducer);

const render = () => {
 const root = ReactDOM.createRoot(
   document.getElementById('root') as HTMLElement
 );
 root.render(
   <React.StrictMode>
     <App
       value={store.getState()}
       onIncrement={() => store.dispatch({ type: 'INCREMENT' })}
       onDecrement={() => store.dispatch({ type: 'DECREMENT' })}
     />
   </React.StrictMode>
 );
};

render();
store.subscribe(render);

Redux Provider

  • react-redux 라이브러리 설치 필요
  1. index.tsx 수정
  • 기존 store.subscribe(render)을 지우고
  • Provider 컴포넌트로 감싸줌.
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { createStore } from 'redux';
import rootReducer from './reducers';
import { Provider } from 'react-redux';

const store = createStore(rootReducer);

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App
        onIncrement={() => store.dispatch({ type: 'INCREMENT' })}
        onDecrement={() => store.dispatch({ type: 'DECREMENT' })}
      />
    </Provider>
  </React.StrictMode>
);
  1. App.tsx 수정
  • 폼 추가 (todos)
  • handleChange, addTodo 함수 작성

참고 - event 객체

  • e: React.ChangeEvent<HTMLInputElement>
  • e: FormEvent<HTMLFormElement>
// App.tsx
import React, { ChangeEvent, FormEvent, useState } from 'react';
import logo from './logo.svg';
import './App.css';

type Props = {
  onIncrement: () => void;
  onDecrement: () => void;
};

function App({ onIncrement, onDecrement }: Props) {
  const [todoValue, setTodoValue] = useState('');

  const addTodo = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setTodoValue('');
  };

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    setTodoValue(e.target.value);
  };

  return (
    <div className="App">
      Clicked: times
      <button onClick={onIncrement}>+</button>
      <button onClick={onDecrement}>-</button>
      <form onSubmit={addTodo}>
        <input type="text" value={todoValue} onChange={handleChange} />
        <input type="submit" />
      </form>
    </div>
  );
}

export default App;

useSelector / useDispatch

  • useSelector = 스토어의 값 가져옴
  • useDispatch = 디스패치 함수 가져옴 (액션 디스패치)
// App.tsx
const counter = useSelector((state) => state.counter);
// Error: 'state'은(는) 'unknown' 형식입니다.ts(18046)
  • state가 unknown 타입임.
    -> rootReducer에서 타입을 생성해주면 해결됨.
    -> 즉, state:RootState와 같이 타입 지정을 해줘야 함.

[참고] ReturnType

// reducer/index.tsx

export type RootState = ReturnType<typeof rootReducer>;
// rootReducer가 반환하는 타입. (ReturnType)                         
// App.tsx
import { RootState } from './reducers';

const todos = useSelector((state: RootState) => state.todos);
const counter = useSelector((state: RootState) => state.counter);


Redux Middleware

로깅 미들웨어

const loggerMiddleware = (store) => (next) => (action) => {
  console.log(store, action);
  next(action)
}

const middleware = applyMiddleware(loggerMiddleware);

const store = createStore(rootReducer, middleware); // 수정

Redux Thunk

  • thunk는 '일부 지연된 작업을 수행하는 코드 조각'을 의미.
  • 즉, 비동기 작업을 해야하는 경우 사용함. (서버에 요청을 보내서 데이터를 가져올 때)

posts 리듀서 생성

enum ActionType {
  FETCH_POSTS = 'FETCH_POSTS',
  DELETE_POSTS = 'DELETE_POSTS',
}

interface Post {
  userId: number;
  id: number;
  title: string;
}

interface Action {
  type: ActionType;
  payload: Post[];
}

const posts = (state = [], action: Action) => {
  switch (action.type) {
    case 'FETCH_POSTS':
      return [...state, ...action.payload];
    default:
      return state;
  }
};

export default posts;
  • App.tsx에 미들웨어 부분 추가
// App.tsx

useEffect(() => {
  dispatch(fetchPosts());
}, [dispatch]);

const fetchPosts = (): any => {
  return async function fetchPostsThunk(dispatch: any, getState: any) {
    const response = await axios.get(
      'https://jsonplaceholder.typicode.com/posts'
    );
    dispatch({ type: 'FETCH_POSTS', text: todoValue });
  };
};

Redux-thunk 적용

  • Action은 객체여야 하는데, dispatch() 안에 함수가 들어가는 경우 에러가 발생한다.
  • 즉, 비동기 함수 등을 디스패치 하려면 redux-thunk 같은 미들웨어 사용이 필요하다.
// index.tsx
import thunk from 'redux-thunk';

const middleware = applayMiddleware(thunk, loggerMiddleware); // thunk를 추가
// App.tsx
const posts: Post[] = useSelector((state: RootState) => state.posts);
...

{posts.map((post, index) => <li key={index}>{post.title}</li>}

폴더 정리

  • 실제로 Redux를 사용하는 경우, 한 곳에 코드를 모두 적지 않고
  • actions 라는 폴더에 액션 파일을 저장함.
// actions/posts.tsx

import axios from 'axios';

const fetchPosts = (): any => {
  return async function fetchPostsThunk(dispatch: any, getState: any) {
    const response = await axios.get(
      'https://jsonplaceholder.typicode.com/posts'
    );
    dispatch({ type: 'FETCH_POSTS', text: response.data });
  };
};

export default fetchPosts;

리팩토링

import axios from 'axios';

export const fetchPosts = () => async (dispatch: any, getState: any) => {
  const response = await axios.get(
    'https://jsonplaceholder.typicode.com/posts'
  );
  dispatch({ type: 'FETCH_POSTS', text: response.data });
};

Redux Toolkit

프로젝트 생성

$ npx create-react-app my-app --template redux-typescript 
  • 기본으로 @reduxjs/toolkit과 react-redux가 install 되어있음.

Redux Toolkit

1. React에 Redux 스토어 제공

  • Provider 컴포넌트

2. Redux State Slice 생성

  • createSlice를 통해 슬라이스 생성
  • Initial State가 무엇인지, Slice 이름이 무엇인지, 어떻게 변경되는지를 선언.
  • Reducer 함수에서 변경 로직을 작성 가능함.
    -> immer 라이브러리를 쓰기 때문에 이전처럼 불변성때문에 머리아플 일 없다!

3. Store에 Slice Reducer 생성

  • 슬라이스에서 리듀서 함수를 가져와서 스토어에 추가 (configureStore)
  • 리듀서 Params 내부에 필드 정의하여 스토어에 리듀서 함수를 사용하여 state를 업데이트하도록.

4. Redux State 및 Actions 사용

  • useSelector, useDispatch 사용

Redux Toolkit APIs

configureStore

createAction

  • 기존에는 액션을 생성할 때, 액션 타입과 생성자 함수를 분리하여 선언했었음.
const INCREMENT = 'counter/increment';  // type

function increment(amount: number) {  // 생성자 함수
  return {
    type: INCREMENT,
    payload: amount
  }
}

const action = increment(10); // action.payload로 10을 넘겨줌
  • createAction을 사용하면 한 번에 처리 가능.
  • type만 넣으면 자동으로 해당 type을 가진 action creater 함수를 생성함.
    (생성된 함수를 호출 시 인수를 넣어준다면, payload로 들어가게 됨.)
import { createAction } from '@reduxjs/toolkit';

const increment = createAction<number>('counter/increment');  // type, 생성자 함수
const action = increment(10);

createReducer

  • switch 문을 통해 구현했던 reducer 함수를 좀 더 간편하게 구현 가능.
  • immer 라이브러리를 내장하여 불변성 유지됨.
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
    })
})

[참고] builder은 무엇인가?

  • createReducer에서 액션 객체를 처리하기 위해 case reducer을 정의하는 방법이 두가지가 있다.
  1. 빌더 콜백 (Builder Callback)
  2. 맵 객체 (Map Object) 표기법.
  • 동일하지만, 빌더 콜백이 TypeScript와의 호환성 때문에 더 선호된다.
  • builder.addCase(type, 생성자) - 액션 타입과 정확히 맵핑되는 케이스 리듀서 추가
  • 그 외에도 addMatcher (주어진 패턴과 일치하는지 체크 후 처리), addDefaultCase 등이 있다.

Prepare 콜백 함수

  • 액션 Contents 커스텀 가능.
  • 예> payload에 사용자 정의 값 추가
import { createAction, nanoid } from '@reduxjs/toolkit'

const addTodo = createAction('todos/add', function prepate(text) {
  return {
    payload: {
      text,
      id: nanoid(),
      createdAt: new Date().toISOString()
    }
  }
})

console.log(addTodo('abc'));

// { type: 'todos/add', payload: {text: 'abc', id: '342413fgkskso', createdAt: '2019-10-03~..'}

createSlice

  • Redux Logic 작성
  • CreateAction + createReducer을 포함함.
  • InitialState, Slice 이름을 받아 -> 액션 생성자와 타입을 자동으로 생성하는 함수.
import { createSlice } from '@reduxjs/toolkit';

const initialState = { value: 0 };

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {     // case reducer 함수들의 객체 
    increment(state) {   // type: counter/increment
      state.value++
    },
    decrement(state) {   // type: counter/decrement
      state.value--
    },
    incrementByAmount(state, action) {   // type: counter/incrementByAmount
      state.value += action.payload
    }
  }
})

extraReducers

  • createSlice가 생성한 action type 외의 다른 액션 타입에 응답 가능
  • 즉, 외부 액션을 참조하기 위한 것.

createAsyncThunk

  • createAction의 비동기 버전.
  • createAction 에서는 type, action을 인수로 넣어주었지만,
  • createAsyncThunk 에서는 type, payloadCreator, options 순으로 넣어준다.
function createAsyncThunk(type, payloadCreator, options)
  • 여기서 type은 user/requestStatus 타입 -> 액션 타입: pending, fulfilled, rejected
  • payloadCreator은 Promise를 반환하는 콜백함수.
    -> 예> 비동기 요청을 보낸 후 response.data를 리턴

참고 문서

cancel

  • useEffect return 부분에 (컴포넌트 언마운트 시) promise.abort()를 실행하여

  • 비동기 요청 도중 취소되도록 구현 가능

  • thunkName/rejected가 디스패치됨 (createAsyncThunk 참고)

  • abort 되었을 때, request도 취소되도록 하려면?
    -> new AbortController()

abortController.signal

profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글