본문 바로가기
학원에서 배운 것/React

KDT 5th 웹개발자 입문 수업 42일차

by 쿠리의일상 2023. 4. 4.

Redux

상태 관리 라이브러리 - 상태 관리가 필요한 다른 프레임워크에서도 사용 가능(jQuery, Vue.js, Angular.js ...)

Redux의 개념을 각각의 프레임워크에 맞춘 라이브러리 사용 -> React-Redux

컴포넌트의 상태를 하나하나의 props 로 전달하면 힘들기에 이를 해결하기 위해 나온 라이브러리

즉, 컴포넌트의 상태를 각각 컴포넌트별 state 에 따라 관리하는 것이 아닌 하나의 store 라는 곳에서 관리한다.

✨ 상태 변화 값을 중첩된 컴포넌트 수 만큼 Props 로 전달하는 방식이 아니라 Store 에서 한번에 꺼내서 사용하는 편리함을 제공한다.

Redux 의 기본 개념

  1. Single source of truth
    • 동일한 데이터는 항상 같은 곳에서 가지고 온다 (스토어라는 하나뿐인 데이터 공간이 존재)
  2. State is read-only
    • 리액트에서는 setState 메서드를 활용해야만 상태 변경이 가능
    • -> 리덕스에서도 액션이라는 객체를 통하여 상태를 변경할 수 있다
    • 즉, 리덕스는 전역 변수로 두는 것과는 달리, 전역으로 사용할 수 있는 상태므로 상태가 변경되면 리렌더링이 발생된다.
  3. Changes are made with pure functions
    • 변경은 순수함수로만 가능
    • 리듀서와 연관된 개념
    • Store - Action - Reducer

 

Redux 동작 순서

1. dispatch() 함수가 실행되면
2. 액션이 발생
3. 이 액션을 리듀서가 받아서
4. Store 안의 상태를 변경 시킴
5. 상태가 변경되면 컴포넌트를 리렌더링 됨

1. Store

상태가 관리되는 하나의 공간, 즉 앱에는 단 하나의 store 가 존재!

컴포넌트와는 별개로 스토어라는 공간이 있어서 그 스토어 안에 앱에서 필요한 상태를 담아준다.

컴포넌트에서 상태 정보가 필요하면 스토어에 접근해줘야 한다.

 

2. Action

앱에서 스토어에 운반할 데이터, 객체 형식으로 되어 있음

 

3. Reducer

Action을 Store 에 바로 전달하는 것이 아닌, Reducer 에 전달해줘야 한다.

리듀서가 요청을 보고 Store의 상태를 업데이트 해주는 것이다.

Action 을 Reducer 에게 전달하기 위해선 dispatch() 메서드를 사용해야 한다.

 

 


npm install redux react-redux 설치

 

 

Redux 기초 세팅

1. <Provider> 로 감싸주기

리덕스 적용을 위해선 <Provider> 컴포넌트를 import 하고 해당 컴포넌트를 <App> 컴포넌트를 감싸주기

import { Provider } from 'react-redux';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <Provider>
      <App />
    </Provider>
  </BrowserRouter>,
);

 

2. Store 만들기

리덕스에서 createStore 를 import 하여 만들어주고,

<Provider> 컴포넌트에게 상태 관리를 할 store 속성에 만들어진 store 를 부여

import { createStore } from 'redux';

지금은 사용되지 않지만 개념상 createStore() 를 사용해줌 

let store = createStore(reducer);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <Provider store={store}>
      <App />
    </Provider>
  </BrowserRouter>,
);

만들어준 store 를 Provider 컴포넌트의 속성에 넣어준다.

 

3. State 값 설정 - 상태 역할

State 는 하나의 변수 또는 객체를 사용

 

4. Reducer 생성

실제적으로 State 값을 관리하는 Reducer 를 함수형태로 만들어주기

State 로 사용할 변수를 매개 변수로 전달해주기

function reducer(state = weight, [Action]) {
  //reducer
  return state;
}

Action에는 상태 관리 가능

 

전체 구조

const weight = 100; //state

function reducer(state = weight) {
  //reducer
  return state;
}

let store = createStore(reducer); //store

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <Provider store={store}> //store를 Provider 에게 전달
      <App />
    </Provider>
  </BrowserRouter>,
);

 

 

Store 에 저장된 값 받아오기 - useSelector

Store 의 상태 값을 받아올 때 React Redux 모듈의 useSelector() 를 사용한다.

import { useSelector } from 'react-redux'
export default function Test() {
  const weight = useSelector((state) => state);

  return <div>당신의 몸무게는 {weight}</div>;
}

 

 

Action 설정

리듀서에게 어떤 처리를 해야하는지 알려주는 역할

 

액션의 객체 내부에 type 이라는 키를 가지고 있으며 해당 type 에는 리듀서에 전달된 액션을 문자열의 형태로 가지고 있다.

Action은 매개변수로 Reducer 에게 전달되며 리듀서는 액션 객체 내부의 type 키 값의 문자열을 읽어서 State 를 어떤 방식으로 처리할 지 결정해준다.

Action : {
	type : 'some text',
}
function reducer(state, action) {
	if(action.type === 'text') {
    	return changeState;
    } else if (action.type === 'etc') {
    	return changeState`;
    } ........
    
	return state;
}

 

Dispatch 로 Action 보내기

컴포넌트에 있는 Action 을 reducer 까지 보내려면 React-Redux 에 있는 useDispatch() 를 사용해야 한다.

  • useSelector() 는 state 값을 가져오는 것
  • useDispatch() 는 state 를 변경시켜줄 action 을 지정해줄 수 있는 것, 액션 발생 함수

Dispatch 로 전달된 Action 의 type 의 값에 따라 Reducer 는 State 값을 변경하고 해당 State 값은 컴포넌트에 반영된다.

import { useDispatch } from 'react-redux';

const dispatch = useDispatch();

 

import { useDispatch, useSelector } from 'react-redux';

export default function Test() {
  const weight = useSelector((state) => state);
  const dispatch = useDispatch();

  return (
    <>
      <h1>당신의 몸무게는 {weight}</h1>
      <button onClick={() => dispatch({ type: '증가' })}>몸무게 증가</button>
      <button onClick={() => dispatch({ type: '감소' })}>몸무게 감소</button>
    </>
  );

 

리덕스를 통한 상태 변경은 리덕스로 관리 중인 상태가 변하면 해당 상태를 사용하는 모든 컴포넌트가 리렌더링된다.

 

 

TodoList

기능 구현

- 할일 추가

- 할일 완료

Store 폴더 생성 - 상태 관리 모듈 사용을 확인 가능

src 폴더 내부에 store 폴더를 만들어주기

store 전체를 총괄하는 모듈은 index.js 를 새로 만들어준다.

Store 모듈 분할하기

모든 컴포넌트에 대한 글로벌 상태값을 하나의 파일에서 관리한다면

해당 파일이 하는 일이 너무 많아지기 때문에

코드의 확장성 및 관리를 위하여 각 기능별 Store 모듈을 분할해준다! (라우터 처럼)

store 폴더 안에 modules 폴더를 둔다.

 

최초의 State 값 설정

컴포넌트가 최초 렌더링 될 때 보여줘야할 최초의 State 값을 설정하기

DB 에서 데이터를 받아서 설정해줘야하지만 일단 기능 구현을 위해 변수로 설정해준다.

id : 고유 id 값 / text : 할일 내용 / done : 완료 여부 로 구성된 객체로 만들어준다.

 

TodoList 이므로 객체가 담긴 배열 형태로 선언해준다.

// 초기 상태
const initState = {
  todoList: [
    {
      id: 0,
      text: '리액트 공부하기',
      done: false,
    },
    {
      id: 1,
      text: '척추 펴기',
      done: false,
    },
    {
      id: 2,
      text: '취업하기',
      done: false,
    },
  ],
};

// Reducer
export default function todo(state = initState, action) {
  return state;
}

 

Store 통합 관리

Store은 모듈별로 관리하여 모듈들(todo.js)은 ----> Store 폴더의 index.js 에 의하여 통합 관리 된다.

index.js 파일 안에 모듈들의 리듀서를 import 해주고 combineReducer() 를 이용하여 리듀서들을 하나로 합쳐서

내보내서 관리해준다.

import { combineReducers } from 'redux';
export default combineReducers({
	// 리듀서들 나열...
  todo,
});

각각의 리듀서들을 combineReducer 를 이용하여 store 모듈에서 export 된 리듀서들을 합쳐준다.

합쳐진 리듀서를 다시 export default 해주는 것임

 

 

Redux 기초 세팅

<Provider> 컴포넌트를 import 하고 해당 컴포넌트로 감싸준다.

src 폴더의 최상위 index.js 에 가서 코드를 처리해준다. (Store 의 index.js 가 아님)

combineReducer 를 통하여 하나로 합쳐서 내보낸 리듀서는----> rootReducer 라는 값으로 받아준다.

import rootReducer from './store/index.js';
// 혹은 import rootReducer from './store'; 로 생략도 가능
const rootReducer = createStore(rootReducer);
//

root.render(
  <Provider store={rootReducer}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>,
);

rootReducer 에는 모듈에 나눠준 리듀서들이 모두 들어있으므로

해당 리듀서를 사용해주기 위해서는

  const weight = useSelector((state) => state.weightReducer);

useSelector() 의 콜백으로 받아주는 인자인 state로 접근해줄 수 있다.

 

 

TodoList 의 기본이 될 컴포넌트

1. 할일 목록을 보여주고 추가 기능을 가지는 <TodoList> 컴포넌트

  • 할일을 추가하는 input, 추가 button
  • 할일 목록을 redux 를 통하여 store 에서 받아온 다음 해당 목록을 ul/li 로 그려주기

2. 완료된 목록을 보여주는 <DoneList> 컴포넌트

3. 두 컴포넌트를 포함하여 전체 앱을 그려주는 <ListContainer> 컴포넌트

 

 

리덕스 상태 관리의 편의 도구

리덕스에 저장된 Store 의 값은 src 폴더의 index.js 파일에서 getState() 메서드를 이용하여 확인

const rootReducer = createStore(combineReducer);
console.log(rootReducer.getState());

하지만 매번 getState() 메서드로 확인해줄 수 없으므로 아래의 확장 프로그램을 사용해준다.

https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd/related?hl=ko 

 

Redux DevTools

Redux DevTools for debugging application's state changes.

chrome.google.com

사용법은 https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd/related?hl=ko  에 존재

 

Redux DevTools

Redux DevTools for debugging application's state changes.

chrome.google.com

 

redux store 를 만들 때 아래의 코드를 삽입해주기

// 리덕스 Dev Tools 전용 코드
const reduxDevTool =
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();

const rootReducer = createStore(combineReducer, reduxDevTool);
console.log(rootReducer.getState());

 

Action 타입 정의

Action 타입은 문자열로 정의한다.

 

앞에 모듈명을 적어주는 것이 약속이다. 다른 store 에서 비슷한 기능을 가지고 있으면 잘못된 사용이 될 수 있기 때문이다.

(모듈이 달라도 Action 타입으로 CREATE/DELETE/DONE 같은 변수명은 많이 쓰임)

//Action
const CREATE = 'todo/CREATE';
const DONE = 'todo/DONE';

Action 생성 함수 작성

외부 컴포넌트에서 Action 을 만들어주는 함수부터 작성

- type 정보와 전달해야할 정보를 payload 객체에 담아서 dispatch 를 통하여 전달한다.

결과적으로 리듀서가 action 함수에 들어있는 type 을 확인하여 어떤 행동을 할지 정하고 payload 에 있는 데이터를 받아서 처리한다.

//Action 생성 함수 CREATE
export function create(payload) {
  return {
    type: CREATE,
    payload,
  };
}
//Action 완료 함수 DONE
export function done(id) {
  return {
    type: DONE,
    id,
  };
}

새로운 정보를 전달할 필요가 없는 done 함수는 어떤 목록이 완료되었는지만 알면 되므로 id 값만 전달하면 된다.

 

위처럼 외부에서 직접 요청하지 않고 모듈 내에서 함수를 import 하여 사용하는 이유는

type 값 등을 외부에서는 알 수 없기 때문에 약속된 함수만 사용하여 접근하는 것이 유지보수나, 편하기 때문임

// 초기 상태
const initState = {
  todoList: [
    {
      id: 0,
      text: '리액트 공부하기',
      done: false,
    },
    {
      id: 1,
      text: '척추 펴기',
      done: false,
    },
    {
      id: 2,
      text: '취업하기',
      done: false,
    },
  ],
};

//Action
const CREATE = 'todo/CREATE';
const DONE = 'todo/DONE';

//Action 생성 함수 CREATE
export function create(payload) {
  return {
    type: CREATE,
    payload,
  };
}

//Action 완료 함수 DONE
export function done(id) {
  return {
    type: DONE,
    id,
  };
}

//reducer
export default function todo(state = initState, action) {
  return state;
}

 

Action Type 에 따라 작동하는 Reducer

switch 문을 사용하여 action type 에 따라서 각각의 역할을 한 뒤 값을 return 하는 구조로 만들어준다.

//reducer
export default function todo(state = initState, action) {
  return state;
}

// Action 을 추가해주기
export default function todo(state = initState, action) {
// 실제 작업이 수행되는 곳!
  switch (action.type) {
    case CREATE:
      return console.log('CREATE');
    case DONE:
      return console.log('DONE');
    default:
      return state;
  }
}

 

Dispatch 로 Action 함수 전달

컴포넌트에서 dispatch 로 정의한 Action 함수(create, done)를 Reducer 에 전달하여 사용

import { create, done } from '../../store/modules/todo';
  const list = useSelector((state) => state.todo.todoList);
  const inputRef = useRef('');
  const dispatch = useDispatch();

  const addTodo = () => {
    dispatch(create(''));
  };

dispatch(Action 함수()) 형태로 실행 가능함

 

Reducer 의 Action 함수 - create 동작 구현

create 의 경우, state 를 전개 연산자로 먼저 리턴해주고 List 의 경우 새롭게 입력 받은 값을 list 의 배열에 넣어준다.

//reducer
export default function todo(state = initState, action) {
  switch (action.type) {
    case CREATE:
      return {
        ...state,
        todoList: state.todoList.concat({
          id: action.payload.id,
          text: action.payload.text,
          done: false,
        }),
        nextId: action.payload.id + 1,
      };
    case DONE:
      return console.log('DONE');
    default:
      return state;
  }
}

dispatch 로 전달해주는 action create 함수는 payload 객체를 보내줘야 하므로 아래처럼 객체로 보내준다.

  const addTodo = () => {
    dispatch(
      create({
        id: list.length,
        text: inputRef.current.value,
      }),
    );
    inputRef.current.value = '';
  };

 

 

Reducer 의 Done Action 함수 동작 구현

동일하게 list 이외의 초기 state 값은 그대로 전달이 되어야 하므로 전개 연산자를 사용한다.

List 의 경우는 컴포넌트에서 전달 받은 id 값과 동일한 객체를 찾은 다음 해당 객체의 done 항목을 true로 변경한다.

//reducer
export default function todo(state = initState, action) {
  switch (action.type) {
    case CREATE:
      return {
        ...state,
        todoList: state.todoList.concat({
          id: action.payload.id,
          text: action.payload.text,
          done: false,
        }),
        nextId: action.payload.id + 1,
      };
    case DONE:
      return {
        ...state,
        todoList: state.todoList.map((e) => {
          if (e.id === action.id) {
            return {
              ...e,
              done: true,
            };
          } else {
            return e;
          }
        }),
      };
    default:
      return state;
  }
}