KDT/TIL

10/7 TIL : Redux, 간단한 Todo-list 만들기

ebulsok 2022. 10. 14. 15:41

🔎 Redux

  • 상태 관리 라이브러리
  • 컴포넌트의 상태를 하나하나 props로 전달하는 것을 방지하고자 생김
  • 컴포넌트의 상태를 store.js 에서 관리

1. dispatch 함수를 실행하면

2. action 발생

3. action을 reducer가 받아서

4. state를 변경

5. state가 변경되면 컴포넌트가 리렌더링

  • [터미널] npm i redux
  • [터미널] npm i react-redux
  • redux 적용을 위해서 <Provider> 컴포넌트를 import하고 <App>을 감싸줘야 함
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';

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

// 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();

 

🚩 store 만들기

  • redux에서 createStore를 임포트 한 뒤 store를 만들고, <Provider>의 store 속성에 부여하기
  • state용 변수 만들기
  • reducer(간단하게 state를 전달만 하는) 만들기
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import { createStore } from 'redux';

const weight = 100;

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

let store = createStore(reducer);

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

// 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();

 

🚩 store에 저장된 값 받아오기

  • react-redux 모듈의 useSelector 사용
// src/components/Test.js
import { useDispatch, useSelector } from "react-redux"

export default function Test() {
    const weight = useSelector((state) => state);
    
    return (
        <>
            <h1>당신의 몸무게는 {weight}</h1>
        </>
    )
}

 

🚩 action 설정

  • action은 reducer에게 어떤 처리를 해야하는지 알려주는 역할
  • 객체 내부에 type 이라는 키를 문자열 형태로 가지고 있음
// src/index.js
	function reducer(state = weight, action) {
          if(action.type === "inc") ++state;
          else if(action.type === 'dec') --state;
          return state;
        }

 

🚩 dispatch로 action 보내기

  • useDispatch()를 하나의 변수에 담고 해당 변수를 통해 컴포넌트의 action 값을 reduer로 전달
// src/component/Test.js
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: "inc" })}}>살 찌우기</button>
            <button onClick={() => { dispatch({ type: "dec" })}}>살 빼기</button>
        </>
    )
}

 

🔎 React TodoList 만들기

  • /src/store 폴더 생성, index.js 파일 추가(store 전체를 총괄하는 모듈)
  • store 모듈 분할: store/modules 폴더 생성, 투두리스트를 관리하는 모듈인 todo.js 파일 추가
  • 초기 state 값 선언: id, text, done
  • 설정한 state 값을 바로 return 시키는 reducer 작성
// src/store/modules/todo.js
// 초기 값 설정
const initState = {
    list: [
        {
            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 통합 관리

  • 초기 값을 선언한 todo.js를 import 해서 todo.js의 reducer를 불러오고, redux의 combineReducer를 이용하여 todo.js의 reducer를 하나로 합쳐서 다시 내보내기
// src/store/index.js
// 통합 관리 파일
import { combineReducers } from "redux";
import todo from "./modules/todo";

export default combineReducers({
    todo,
})

 

🚩 redux 기초 세팅

  • combineReducer를 통해 하나로 합쳐서 내보낸 reducer를 rootReducer 라는 값으로 받기
// src/index.js
import rootReducer from './store';

const store = createStore(rootReducer);

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

 

🚩 <TodoList>, <DoneList>, <ListContainer> 컴포넌트 제작

// src/components/TodoList.js
import { useRef } from 'react'
import { useSelector } from 'react-redux'

export default function TodoList() {
    const list = useSelector((state) => state.todo.list);
    const inputRef = useRef();

    return (
        <section>
            <h1>할 일 목록</h1>
            <div>
                <input type="text" ref={inputRef} />
                <button>추가</button>
            </div>
            <ul>
                {list.map((el) => {
                    return (
                        <li key={el.id}>
                            {el.text}
                        </li>
                    )
                })}
            </ul>
        </section>
    )
}
// src/components/DoneList.js
import { useSelector } from 'react-redux'

export default function DoneList() {
    const list = useSelector((state) => state.todo.list);
    return (
        <section>
            <h1>완료된 목록</h1>
            <ul>
                {list.map((el) => {
                    return (
                        <li key={el.id}>
                            {el.text} 
                        </li>
                    )
                })}
            </ul>
        </section>
    )
}
// src/components/ListContainer.js
import DoneList from "./DoneList";
import TodoList from "./TodoList";

export default function ListComponent() {
  return (
    <>
        <TodoList />
        <DoneList />
    </>
  )
}

 

🚩 action 타입 정의, action  생성 함수, reducer 구조 구현

  • action 생성 함수는 type 정보와 전달해야 할 정보를 payload 객체에 담아서 dispatch를 통해 전달
  • 결과적으로 reducer가 action 함수에 들어있는 type을 확인해서 어떤 행동을 할지 정하고, payload에 있는 데이터를 받아서 처리
// src/store/modules/todo.js
// 액션 타입 정의
const CREATE = "todo/CREATE";
const DONE = "todo/DONE";

// 액션 함수
export function create(payload) {
    return {
        type: CREATE,
        payload,
    }
}

export function done(id) {
    return {
        type: DONE,
        id,
    }
}

// reducer
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 활용을 위해 useDispatch를 변수에 넣어주기
  • todo.js에서 create, done 함수 불러오기
  • dispatch의 인자로 create, done 함수를 전달하여 호출 상태 확인
// src/components/TodoList.js
import { useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { create, done } from '../store/modules/todo';

export default function TodoList() {
    const list = useSelector((state) => state.todo.list);
    const inputRef = useRef();
    const dispatch = useDispatch();

    return (
        <section>
            <h1>할 일 목록</h1>
            <div>
                <input type="text" ref={inputRef} />
                <button onClick={() => { dispatch(create('') }>추가</button>
            </div>
            <ul>
                {list.map((el) => {
                    return (
                        <li key={el.id}>
                            {el.text}
                        </li>
                    )
                })}
            </ul>
        </section>
    )
}

 

🚩 reducer CREATE/DONE 구현

  • 혹시 모를 다른 초기 값이 있을 지 모르므로 state를 전개 연산자로 먼저 리턴
  • CREATE: List의 경우 새롭게 입력 받은 값을 list의 배열에 넣어주기
  • DONE: List의 경우 컴포넌트에서 전달 받은 id 값과 동일한 객체를 찾은 다음 해당 객체의 done 항목을 true로 변경
// src/store/modules/todo.js
export default function todo(state = initState, action) {
    switch(action.type) {
        case CREATE:
            return {
                ...state,
                list: state.list.concat({
                    id: action.payload.id,
                    text: action.payload.text,
                    done: false,
                })
            }
        case DONE:
            return {
                ...state,
                list: state.list.map((el) => {
                    if(el.id === action.id) return {
                        ...el,
                        done: true,
                    }; else return el;
                })
            }
        default:
            return state;
    }
}

 

🚩 dispatch로 CREATE/DONE 호출

// src/components/TodoList.js
return (
        <section>
            <h1>할 일 목록</h1>
            <div>
                <input type="text" ref={inputRef} />
                <button onClick={() => {
                    dispatch(create({ id: list.length, text: inputRef.current.value }));
                    inputRef.current.value = "";    
                }}>추가</button>
            </div>
            <ul>
                {list.map((el) => {
                    return (
                        <li key={el.id}>
                            {el.text}
                            <button onClick={() => { dispatch(done(el.id)) }}>완료</button>
                        </li>
                    )
                })}
            </ul>
        </section>
    )

 

🚩 각각의 컴포넌트에 filter 처리

// src/components/TodoList.js
export default function TodoList() {
    const list = useSelector((state) => state.todo.list).filter(
        (el) => el.done === false
    );
    // ...
}
// src/components/DoneList.js
export default function DoneList() {
    const list = useSelector((state) => state.todo.list).filter(
        (el) => el.done === true
    );
    // ...
}

 

🚩 List 요소의 key 값이 고유하도록 해당 순번도 store에서 전역으로 관리하여 처리

// src/store/modules/todo.js
let counts = initState.list.length;
initState['nextID'] = counts;

export default function todo(state = initState, action) {
    switch(action.type) {
        case CREATE:
            return {
                ...state,
                list: state.list.concat({
                    id: action.payload.id,
                    text: action.payload.text,
                    done: false,
                }),
                nextID: action.payload.id + 1,
            }
        case DONE:
        // ...
}
// src/components/TodoList.js
const nextID = useSelector((state) => state.todo.nextID);

return (
        <section>
            <h1>할 일 목록</h1>
            <div>
                <input type="text" ref={inputRef} />
                <button onClick={() => {
                    dispatch(create({ id: nextID, text: inputRef.current.value }));
                    inputRef.current.value = "";    
                }}>추가</button>
            </div>
            // ...
)

 

📌 코드: https://github.com/ebulsok/KDT-React-Todolist.git