본문 바로가기

카테고리 없음

React 스터디 - Redux

<이론 Part>

리덕스란?

Redux는 자바스크립트 애플리케이션을 위한 상태 관리 라이브러리이다.

Redux는 Satate를 관리하는 라이브러리이다!! 

 

[Redux의 data flow]

Action -> (Call the reducer) -> Reducer ->(Update Store) -> Redux Store -> (Render new view) -> React Component ->(Dispatch Synchronous Action) ->다시 Action으로 회귀...

 

*Action: 간단한 자바스크립트 객체이다. 여기에는 우리가 수행하는 작업의 유형을 지정하는 type 속성 있다. 선택적으로 redux 저장소에 일부 데이터를 보내는데 사용되는 payload 속성을 가질 수 도 있다.

 

*Reducer: 애플리케이션 상태의 변경 사항을 결정하고 업데이트된 상태를 반환하는 함수이다. 

전체적인 구조도

*Redux Store: 이들을 하나로 모으는 객체 저장소로, 애플리케이션의 전체 상태 트리를 보유한다. 내부 상태를 변경하는 유일한 방법은 해당 상태에 대한 Action을 전달하는 것 뿐이다. 리덕스 스토어는 클래스가 아니라, 몇가지 method를 가진 객체일 뿐이다.

 

 

+ Bonus) Props와 State의 차이는?

1) Props

Props는 properties의 줄임말로, 구성요소가 서로 통신하는 방법이다.

상위 구성 요소에서 아래쪽으로 props를 전달하는 흐름을 가진다.

해당 값을 변경하려면 자식 관점에서 props를 변경할 수가 있다. 부모는 내부 상태를 변경해야 한다.

Props 사용 예시

 

2) State

부모 컴포넌트에서 자식 컴포넌트로 전달 X.

해당 컴포넌트 안에서 data를 전달할 때 State를 사용한다.

State가 변하면 re-render 된다.

ex) 검색창에 글을 입력할 때 글이 변하는 것은 state를 바꾸는 것이다.

State 사용 예시

 

 

 

리덕스 미들웨어란?

 

Redux middleware dispatch 전달 후 reducer에 도달하는 순간 사이에 사전에 지정된 작업을 실행할 수 있게 해주는 중간자이다. 로깅, 충돌 보고, 비동기 API와 통신, 라우팅 등을 위해 Redux 미들웨어를 사용한다.

 

아래와 같이 로깅 미들웨어 함수를 생성한다.

실습Part App.tsx 파일을 보면, 로깅 미들웨어 함수에 대한 코드를 볼 수 있다.

 

 

리덕스 Thunk란?

 

리덕스를 사용하는 앱에서 비동기 작업을 할 때 redux-thunk를 많이 사용한다.

비동기 요청은 보통 서버에 요청 보내서 데이터를 가져올때 보낸다.

이것 또한 logger 미들웨어처럼 리덕스 미들웨어이며, 리덕스를 개발한 사람이 만들었다.

 

*Thunk: 일부 지연된 작업을 수행하는 코드 조각을 의미하는 프로그래밍 용어이다. 지금 일부 논리를 실행하는 대신 나중에 작업을 수행하는 데 사용할 수 있는 함수 본문이나 코드를 작성할 수 있다.

 

 

 

 

<실습 Part>

코드 작성 후 블로그 정리를 시작하여 순서가 뒤죽박죽일 수 있습니다.

*개발환경 셋팅

작업폴더 생성하여 리액트 앱 설치 + typescript도 이용할 것이므로 해당 모듈도 함께 설치 + redux 설치

npx create-react-app my-app --template typescript
npm install redux --save
//Axios 모듈 나중에 리덕스 Thunk할때 사용해야하므로 redux-thunk와 함께 미리 설치해두자.
npm install axios --save
npm install redux-thunk --save

 

 

1) App의 전체 상태 트리를 보유하는 Redux 저장소 생성. 앱 하나에는 무조건 하나의 스토어만 있어야한다.

-App.tsx 파일

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

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

const loggerMiddleware = (store:any)=>(next:any)=>(action:any)=>{
  console.log("store",store);
  console.log("action",action);
  next(action);
}
const middleware=applyMiddleware(thunk, loggerMiddleware);
const store=createStore(rootReducer);
store.dispatch({
  type:'ADD_TODO',
  text:"Use_Redux"
})
console.log('store.getState()',store.getState());

const render=()=>root.render(
  <React.StrictMode>
    <Provider store={store} >
    <App
    value={store.getState()}
    onIncrement={()=>store.dispatch({type:"INCREMENT"})}
    onDecrement={()=>store.dispatch({type:"DECREMENT"})}
    />
    </Provider>
  </React.StrictMode>
);
//이제 +,- 연산 제대로 작동.
render();
store.subscribe(render);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

애플리케이션의 현재 상태트리를 반환한다. 스토어의 리듀서가 반환한 마지막 값과 같다.

 

 

2) reducers 폴더 안에 counter 앱을 생성한다.

앞으로 만들 todo(할일 추가) 기능과 posts(post 조회) 기능에 대한 .tsx 파일 또한 생성하였다.

 

-index.tsx 파일

import {combineReducers} from 'redux';
import counter from './counter';
import todos from './todos';
import posts from './posts';

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

export default rootReducer;

export type RootState=ReturnType<typeof rootReducer>;

여러 Reducer를 하나로 결합해 애플리케이션의 전체 상태를 관리하기 위한 코드이다.

해당 코드는 counter, todos, posts 총 세개의 리듀서를 하나의 루트 리듀서로 결합해주는 코드이다.

각 counter, todos, posts에 대한 상태를 관리하는 코드는 아래애서 작성할 것이다.

 

[각 리듀서가 수행하는 동작]

*counter: +/- 버튼에 따라 증감 연산 수행함.

*todos: 할 일 목록 관련 상태를 추가 및 조회할 수 있게 함.

*posts: 게시물 목록을 웹페이지에 로드함.

 

 

-counter.tsx 파일

interface Action{
    type:string
}

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

export default counter;

 

INCREMENT action을 수행하면 state가 1 증가하고, DECREMENT action을 취하면 state가 1 감소하도록 counter함수를 작성했다.

 

-todos.tsx 파일

enum ActionType{
    ADD_TODO="ADD_TODO",
    DELETE_TODO="DELETE_TODO"
}

interface Action{
    type:ActionType;
    text:string
}
const todos=(state=[], action:Action)=>{
    switch(action.type){
        case "ADD_TODO":
            return [...state,action.text]
       
        default:
            return state;
    }
}

export default todos;

ADD_TODO action 수행 시, 기존 내용에 새롭게 추가된 action.text를 추가해서 할 일 목록을 생성한다.

 

-posts.tsx

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;

외부 API에서 게시물에 대한 내용을 가져와 웹페이지상에 띄우는 역할을 한다.

 

 

-App.tsx 파일

todos, counter, posts action을 수행하는 함수에 대한 UI를 생성하였다.(App.tsx)

=> props 타입과 post 인터페이스를정의했다.

 props는 컴포넌트에 전달되는 속성을, post는 게시물 데이터의 구조를 정의한다.

 

=> App 컴포넌트에서 리덕스의 useDispatch와 useSelector를 사용해서 리덕스와의 상호작용을 설정한다.

useEffect를 사용해 컴포넌트가 마운트 될 때 게시물을 가져오는 작업을 수행한다.

 

=> fetchPosts 함수는 비동기 thunk 함수를 반환한다. 이 함수는 axios를 사용해 외부 API에서 게시물 데이터를 가져온다.

가져온 데이터는 'FETCH_POSTS' action type과 함께 dispatch된다.

 

=> 이벤트 핸들러는 handleChange와 addTodo가 있는데, handleChange는 todo 입력필드의 변경을 처리하고, addTodo는 새 todo를 추가하는 폼 제출을 처리한다. 

 

=> UI 부분에서는 Redux 스토어의 counter, todos, posts 상태를 사용해 화면에 정보를 표시한다. 각 todo와 게시물은 리스트로 표시된다.

 

=> counter, todos, posts 상태는 Redux 스토어에서 가져온다. 버튼을 클릭하면 onIncrement, onDecrement 함수가 각각 호출돼서 counter 값을 변경한다.(증감연산 수행). todo에 대해서는 form 제출 시 ADD_TODO 액션과 함께 Redux 스토어로 Dispatch되어서 todo리스트에 해당 항목이 추가되게 된다.

 

이와 같이 Redux를 사용해 애플리케이션의 상태를 관리하고, axios로 외부 API에서 데이터를 가져와 화면에 표시하는 간단한 상태관리앱을 실습해보았다.

 

 

import React,{useState,useEffect} from 'react';
import axios from 'axios'
import './App.css';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from './reducers';

//props type 정의 필요(typeScript 사용시)
type Props={
  value:any;
  onIncrement:()=>void;
  onDecrement:()=>void;
}
interface Post{
  userId:number;
  id:number;
  title:string;
}
//App에 props 가져오기
function App({value, onIncrement, onDecrement}:Props) {
  const dispatch=useDispatch();
  const counter=useSelector((state:RootState)=>state.counter);
  const todos: string[]=useSelector((state:RootState)=>state.todos);
  const posts:Post[]=useSelector((state:RootState)=>state.posts);
  const [todoValue, setTodoValue]=useState("");

  // useEffect(()=>{
  //   dispatch(fetchPosts());
  // },[dispatch])
 
  const fetchPosts=()=>{
    return async function fetchPostsThunk(dispatch:any, getState:any){
      const response = await axios.get("https://jsonplaceholder.typicode.com/posts");
      dispatch({type:"FETCH_POSTS",payload:response.data})
    }
  }
  const handleChange=(e: React.ChangeEvent<HTMLInputElement>)=>{
    setTodoValue(e.target.value);
  }
  const addTodo=(e:React.FormEvent<HTMLFormElement>):void=>{
    e.preventDefault();
    dispatch({type:"ADD_TODO",text:todoValue})

    setTodoValue("");
  }

  return (
    <div>
      Clicked: {counter} times  
      {' '}
      <button onClick={onIncrement}>
        +
      </button>
      <button onClick={onDecrement}>
        -
      </button>

      <ul>
        {todos.map((todo,index)=><li key={index}>{todo}</li>)}
      </ul>

      <form onSubmit={addTodo}>
        <input type="text" value={todoValue} onChange={handleChange} />
        <input type="submit" />
      </form>
      <ul>
        {posts.map((post,index)=><li key={index}>{post.title}</li>)}
      </ul>
    </div>

   
  );
}

export default App;

 

   
 

 

npm run start를 실행한 모습.