옝옹
냠
옝옹
전체 방문자
오늘
어제
  • 분류 전체보기 (84)
    • [LG유플러스]유레카 SW (5)
    • React (20)
    • JS (17)
    • TypeScript (5)
    • CSS & HTML (1)
    • 알고리즘 (11)
    • JAVA (20)
    • GIT (1)
    • 자격증 (4)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 혼자공부하는자바
  • 자바
  • js
  • 혼자 공부하는 자바
  • 인스턴스멤버
  • TypeScript
  • Node.js
  • 타입변환
  • fillter
  • 코드스플리팅
  • java
  • 함수선언
  • 정적멤버
  • reduce
  • join() 메서드
  • While문
  • join()
  • sort() 메서드
  • 노마드코더
  • map
  • template literal
  • do-while문
  • 자바스크립트
  • 자바스트립트
  • switch문
  • 타입스크립트
  • reverse() 메서드
  • 리액트를다루는기술
  • ==
  • 템플릿리터럴
  • 백준
  • 접근제한자
  • useEffect
  • useCallback
  • java.util패키지
  • 변수선언
  • map() 함수
  • toFixed
  • continue문
  • 기본api클래스
  • useState
  • match()
  • JavaScript
  • useRef
  • 리액트
  • break문
  • 화살표함수
  • java.lang패키지
  • indexOf()
  • useMemo

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
옝옹

냠

[React] 리덕스를 사용해 상태 관리
React

[React] 리덕스를 사용해 상태 관리

2023. 4. 6. 03:34
  • 리덕스를 사용할 때는 액션 타입, 액션 생성 함수, 리듀서 코드를 작성해야 함
  • 이 코드를 각각 다른 파일에 작성해도되고 기능별로 묶어서 파일 하나에 작성해도 됨

 

 

  • actions, constants, reducers라는 세 개의 디렉터리를 만드는 방식
  • 편리하지만 새로운 액션을 만들 때 세 종류의 파일을 모두 수정해야 하여 불편함
  • 리덕스 공식 문서에서 사용되는 가장 일반적인 방식

 

 

Ducks 패턴

  • 액션 타입, 액션 생성함수, 리듀서 함수를 기능별로 파일 하나에 몰아서 다 작성하는 방식
  • Ducks 패턴을 사용해 액션 타입, 액션 생성 함수, 리듀서를 작성한 코드를 '모듈' 이라고 한다

[counter 모듈 ( modules/counter.js]

1. 액션 타입을 정의

- 액션 타입은 대문자로 정의

- 문자열 내용은 '모듈 이름/액션 이름'과 같은 형태로 작성

- 문자열 안에 모듈 이름을 넣음으로써 나중에 프로젝트가 커졌을 때 액션의 이름이 충돌되지 않게 해줌

const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

2. 액션 생성 함수 만들기

주의해야 할 점 : 앞 부분에 export라는 키워드가 들어가므로써 추후 이 함수를 다른 파일에서 사용 가능

export const increase = () => ({type : INCREASE});
export const decrease = () => ({type : DECREASE});

3. 초기 상태 및 리듀서 함수 만들기

- 초기 상태에는 number 값을 설정해줌

- 리듀서 함수에는 현재 상태를 참조하여 새로운 객체를 생성해서 반환하는 코드를 작성

const initialState = {
    number : 0
};

function counter(state = initialState, action) {
    switch (action.type) {
        case INCREASE:
            return {
                number: state.number + 1
            };
        case DECREASE:
            return {
                number: state.number - 1
            };
        default:
            return state;
    }
}

전체 코드

// 1. 액션 타입을 정의
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

// 2. 액션 생성 함수 만들기
export const increase = () => ({type : INCREASE});
export const decrease = () => ({type : DECREASE});

// 3. 초기 상태 및 리듀서 함수 만들기
const initialState = {
    number : 0
};

function counter(state = initialState, action) {
    switch (action.type) {
        case INCREASE:
            return {
                number: state.number + 1
            };
        case DECREASE:
            return {
                number: state.number - 1
            };
        default:
            return state;
    }
}

export default counter;
export : 여러개를 내보낼 수 있음
export default : 단 한개만 내보낼 수 있음
→ 불러오는 방식도 다름
import counter from './counter';
import {increase, decrease} from './counter';
// 한꺼번에 불러오고 싶을 때
import counter, {increase, decrease} from './counter';​

[counter 모듈 ( modules/counter.js]

  • createAction을 사용해 매번 객체를 만들어 줄 필요 없이 간단하게 액션 생성 함수를 선언
  • handleActions을 사용해 리듀서 함수를 가독성 높게 작성 가능
import {createAction, handleActions} from 'redux-actions';

// 1. 액션 타입 정의
const CHANGE_INPUT = "todos/CHANGE_INPUT"; // 인풋 값을 변경함
const INSERT = 'todos/INSERT'; // 새로운 todo를 등록함
const TOGGLE = 'todos/TOGGLE'; // todo를 체크/체크 해제함
const REMOVE = 'todos/REMOVE'; // todo를 제거함

// 2. 액션 생성 함수 만들기
// 액션 생성 함수에서 파라미터가 필요함
// 전달 받은 파라미터는 액션 객체 안에 추가 필드로 들어가게 됨
export const changeInput = createAction(CHANGE_INPUT, input => input);

// 액션 객체를 만들 때 파라미터 외에 사전에 이미 선언되어 있는 id값에 의존함
// 이 액션 생성 함수(insert 함수)는 호출될 때마다 id값에 1씩 더해줌
// 이 id값은 각 todo 객체가 들고 있게 될 고유값
let id = 3;
export const insert = createAction(INSERT, text => ({
    id: id++,
    text,
    done: false,
}));
// export const insert = text => ({
//     type : INSERT,
//     todo : {
//         id : id++,
//         text,
//         done: false
//     }
// });

export const toggle = createAction(TOGGLE, id => id);
export const remove = createAction(REMOVE, id => id);
// export const toggle = id => ({
//     type : TOGGLE,
//     id
// });

// export const remove = id => ({
//     type : REMOVE,
//     id
// });

// 3. 초기 상태 및 리듀서 함수 만들기
const initialState = {
    input: '',
    todos: [
        {
            id: 1,
            text: "리덕스 기초 배우기",
            done: true
        },
        {
            id: 2,
            text: "리액트와 리덕스 사용하기",
            done: false
        }
    ]
};

const todos = handleActions(
    {
        [CHANGE_INPUT]: (state, { payload: input}) => ({...state, input}),
        [INSERT]: (state, {payload: todo}) => ({
            ...state,
            todos: state.todos.concat(todo),
        }),
        [TOGGLE]: (state, {payload: id}) => ({
            ...state,
            todos: state.todos.map(todo =>
                todo.id === id ? {...todo, done: !todo.done} : todo,
            ),
        }),
        [REMOVE]: (state, {payload: id}) => ({
            ...state,
            todos: state.todos.filter(todo => todo.id !== id),
        }),
    },
    initialState,
);

// function todos(state = initialState, action) {
//     switch (action.type) {
//         case CHANGE_INPUT:
//             return {
//                 ...state,
//                 input: action.input
//             };
//         case INSERT:
//             return {
//                 ...state,
//                 todos: state.todos.concat(action.todo)
//             };
//         case TOGGLE:
//             return {
//                 ...state,
//                 todos: state.todos.map(todo => 
//                     todo.id === action.id ? {...todo, done: !todo.done} : todo
//                 )
//             };
//         case REMOVE:
//             return {
//                 ...state,
//                 todos: state.todos.filter(todo => todo.id !== action.id)
//             };
//         default:
//             return state;
//     }
// }

export default todos;

4. 루트 리듀서 만들기

  • 여러개 만든 리듀서를 combineReducer라는 유틸 함수를 사용해 하나로 합쳐줌
    • createStore함수를 사용해 스토어를 만들 때는 리듀서를 하나만 사용해야 하기 때문
// modules/index.js
import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';

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

export default rootReducer;

[리액트 애플리케이션에 리덕스 적용]

1. 스토어 만들기

2. Provider 컴포넌트를 사용해 프로젝트에 리덕스 적용시킴

  • 리액트 컴포넌트에서 스토어를 사용할 수 있도록 Provider 컴포넌트로 감싸 줌
  • 이 컴포넌트를 사용할 때는 store를 props로 전달해줘야함
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
import { BrowserRouter } from "react-router-dom";
import { createStore } from 'redux';
import rootReducer from './modules';
import {Provider} from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension'; // 리덕스 개발자 도구

const store = createStore(rootReducer, composeWithDevTools()); // 스토어를 만듭니다.

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

[컨테이너 컴포넌트 만들기]

  • 리덕스 스토어에 접근해 원하는 상태를 받아 오고, 액션도 디스패치 해줘야함
    • 리덕스 스토어와 연동된 컴포넌트를 컨테이너 컴포넌트라고 부름
// containers/CounterContainer.js
import {useCallback} from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import {increase, decrease} from '../modules/counter';
import {useSelector, useDispatch} from 'react-redux';

const CounterContainer = () => {
    const number = useSelector(state => state.counter.number);
    const dispatch = useDispatch();
    const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
    const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);
    
    return (
        <Counter
            number = {number} 
            onIncrease = {onIncrease}
            onDecrease = {onDecrease}
        />
    );
};

export default CounterContainer;
<!--
const mapStateToProps = state => ({
     number: state.counter.number,
});

const mapDispatchToProps = dispatch => ({
     // 액션 생성 함수를 불러와 액션 객체를 만들고 디스패치해줌
     increase: () => {
         dispatch(increase());
     },
     decrease: () => {
         dispatch(decrease());
     },
});

두번째 파라미터를 아예 객체 형태로 넣어 주면 connect함수가 내부적으로 bindActionCreators 작업을 대신 해줌
export default connect(
     state => ({
         number: state.counter.number,
     }),
     {
         increase,
         decrease,
     },
)(CounterContainer);
mapDispatchToProps에 해당하는 파라미터를 함수 형태가 아닌 액션 생성 함수로 이루어진 객체 형태로 넣어줌
-->
컴포넌트를 리덕스와 연동하려면 connect 함수를 사용해야 함
const makeContainer = connect(mapStateToProps, mapDispatchToProps)
makeContainer(타깃 컴포넌트)

- mapStateToProps : 리덕스 스토어 안의 상태를 컴포넌트의 props로 넘겨주기 위해 설정하는 함수
- mapDispatchToProps : 액션 생성 함수를 컴포넌트의 props로 넘겨주기 위해 사용하는 함수

→ connect 함수를 호출하고 나면 또 다른 함수를 반환
→ 반환된 함수에 컴포넌트를 파라미터로 넣어 주면 리덕스와 연동된 컴포넌트가 만들어짐

[ TodosContainer 만들기]

// containers/TodosContainer.js
import { connect } from 'react-redux';
import { changeInput, insert, toggle, remove } from '../modules/todos';
import Todos from '../components/Todo';

const TodosContainer = ({
    input,
    todos,
    changeInput,
    insert,
    toggle,
    remove,
}) => {
    return (
        <Todos
            input={input}
            todos={todos}
            onChangeInput={changeInput}
            onInsert={insert}
            onToggle={toggle}
            onRemove={remove}
        />
    );
};

export default connect(
    <!-- 비구조화 할당을 통해 todos를 분리하여
     state.todos.input 대신 todos.input을 사용 -->
    ({todos}) => ({
        input: todos.input,
        todos: todos.todos,
    }),
    {
        changeInput,
        insert,
        toggle,
        remove,
    },
)(TodosContainer);

Hooks를 사용해 컨테이너 컴포넌트 만들기

1. useSelector로 상태 조회useSelector Hook을 사용하면 connect 함수를 사용하지 않고도 리덕스의 상태를 조회할 수 있음

const 결과 = useSelector(상태 선택 함수);

2. useDispatch를 사용해 액션 디스패치하기

컴포넌트 내부에서 스토어의 내장 함수 dispatch를 사용할 수 있게 해줌

→ 컨테이너 컴포넌트에서 액션을 디스패치해야한다면 이 Hook을 사용하면 됨

→ 컴포넌트 성능을 최적화해야 하는 상황이 온다면 useCallback으로 액션을 디스패치하는 함수를 감싸주는게 좋음

// containers/CounterContainer.js
import {useCallback} from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import {increase, decrease} from '../modules/counter';
import {useSelector, useDispatch} from 'react-redux';

const CounterContainer = () => {
//1. connect 함수 대신 useSelector를 사용해 counter.number값을 조회함으로써 counter에게 props를 넘겨줌
    const number = useSelector(state => state.counter.number);
    const dispatch = useDispatch();
    const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
    const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);
    
    return (
        <Counter
            number = {number} 
            onIncrease = {onIncrease}
            onDecrease = {onDecrease}
        />
    );
};

connect 함수와 리덕스 관련 Hook(useSelector, useDispatch)의 주요 차이점

  • connect 함수를 사용해 컨테이너 컴포넌트를 만들었을 경우, 해당 컨테이너 컴포넌트의 부모 컴포넌트가 리렌더링될 때 해당 컨테이너 컴포넌트의 props가 바뀌지 않았다면 리렌더링이 자동으로 방지되어 성능이 최적화됨
  • 반면 useSelector를 사용해 리덕스 상태를 조회했을 때는 이 최적화 작업이 자동으로 이루어지지 않으므로, 성능 최적화를 위해서는 React.memo를 컨테이너 컴포넌트에 사용해주어야 한다.
    • 이번 예시(TodosContainer)에서는 부모 컴포넌트인 App 이 리렌더링되는 일이 없으므로 불필요한 최적화임

 

총 코드

https://github.com/yeeun426/recordMe_web

 

GitHub - yeeun426/recordMe_web

Contribute to yeeun426/recordMe_web development by creating an account on GitHub.

github.com

 

저작자표시 비영리 변경금지 (새창열림)

'React' 카테고리의 다른 글

[React] 코드 스플리팅  (1) 2023.05.01
[React] 리덕스 미들웨어를 통한 비동기 작업 관리  (0) 2023.04.14
[React] 리덕스 라이브러리  (0) 2023.04.05
[React] Context API  (0) 2023.04.05
[React] 라우팅  (0) 2023.03.30
    'React' 카테고리의 다른 글
    • [React] 코드 스플리팅
    • [React] 리덕스 미들웨어를 통한 비동기 작업 관리
    • [React] 리덕스 라이브러리
    • [React] Context API
    옝옹
    옝옹

    티스토리툴바