버전에 맞춰서 강의와 다른 코드가 있습니다.

강의와 별개로 추가한 내용입니다.

 

결과 화면

 


redux-persist 패키지를 사용해서 데이터를 로컬 스토리지에 저장했다.

yarn add redux-persist 또는 npm install redux-persist

 

파일 구조

  • src/index.js

1. persistStore, PersistGate를 import 시킨다.

2. persistStore(store)를 만든다.

3. PersistGate 태그로 App 컴포넌트를 감싼다.

import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { persistStore } from 'redux-persist';
import { PersistGate } from 'redux-persist/integration/react';
import store from './store/store'
import App from './App'

const root = ReactDOM.createRoot(document.getElementById('root'))
const persistor = persistStore(store)

root.render(
  <Provider store={store}>
    <PersistGate loading={null} persistor={persistor}>
      <App/>
    </PersistGate>
  </Provider>
)

 

  • src/store/rootReducer.js

1. store 디렉터리에 rootReducer.js 파일을 만든다.

2. combineReducers()에 기존에 만든 reducer를 작성한다. (toDos 이외에 리듀서가 여러 개라면 쉼표로 추가해 작성한다.)

import { combineReducers } from 'redux'
import toDos from './toDoReducer'

const rootReducer = combineReducers({
  toDos
})

export default rootReducer

 

  • src/store/store.js

1. store와 reducer를 나눠서 작성한다. (store 관련 코드만 남긴다.)

2. persistReducer를 import 시키고, persistReducer()에 rootReducer를 파라미터로 가지고 온다.

3. persistConfig 변수를 만들고, configureStore()에 리듀서를 작성한다.

import { configureStore } from '@reduxjs/toolkit'
import { persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import rootReducer from './rootReducer'

const persistConfig = {
  key: 'root',
  storage
}

const reducer = persistReducer(persistConfig, rootReducer)

const store = configureStore({
  reducer
})

export default store

 

  • src/store/toDoReducer.js

1. store와 reducer를 나눠서 작성한다. (reducer관련 코드만 남긴다. 파일명이 헷갈릴 수 있으니 리네이밍한다.)

import { createSlice } from '@reduxjs/toolkit'

const todoSlice = createSlice({
  name: 'reducerToDo',

  initialState: {
    toDoList: []
  },

  reducers: {
    addToDo: (state, action) => {
      state.toDoList.unshift({ id: Date.now(), text: action.payload })
    },
    deleteToDo: (state, action) => {
      state.toDoList = state.toDoList.filter(list => list.id !== action.payload)
    }
  }
})

export const { addToDo, deleteToDo } = todoSlice.actions
export default todoSlice.reducer

 

  • src/routes/Home.js

1. useSelector()로 state를 가지고 올 때, 로컬 스토리지에 있는 데이터(state.toDos.toDoList)를 가지고 온다.

import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { addToDo } from '../store/toDoReducer'
import ToDo from '../components/ToDo'
import './home.scss'

function Home() {
  // ** Hooks
  const dispatch = useDispatch()

  // ** States
  const [text, setText] = useState('')

  // ** Redux States
  const toDos = useSelector(state => state.toDos.toDoList)

  function onChange(e) {
    setText(e.target.value)
  }

  function onSubmit(e) {
    e.preventDefault()
    dispatch(addToDo(text))
    setText('')
  }

  return (
    <>
      <div className="layout home">
        <h1>To Do List</h1>

        <form onSubmit={onSubmit}>
          <input type="text" value={text} onChange={onChange} placeholder="What is your to do?"/>
          <button>Add</button>
        </form>

        <ul>
          {
            toDos.map((toDo, index) => {
              return (
                <ToDo id={toDo.id} text={toDo.text} key={index}/>
              )
            })
          }
        </ul>
      </div>
    </>
  )
}

export default Home

 

 

자세한 소스코드는 아래 깃 레포지토리를 참고한다.

https://github.com/heejae0811/react-todo

버전에 맞춰서 강의와 다른 코드가 있습니다.

 

결과 화면

 


Redux Toolkit 기본 개념

  • configureStore()

기존 createStore 역할을 대신해 사용한다.

configureStore()는 리듀서 조각들을 자동으로 합쳐주고, 기본으로 제공되는 기능뿐만 아리나 Redux DevTools 확장을 사용할 수 있게 해준다.

 

  • createSlice()

기존 reducer에서 switch문 역할을 대신해 사용한다.

초기 state와 리듀서 함수를 작성할 수 있고, array.push와 같은 state를 직접 수정할 수 있다.

state는 mutate 하면 안되는 것이 원칙인데, 리덕스 툴깃이 자동으로 새로운 state로 반환시켜준다.

 


 

redux toolkit을 사용하기 위해 터미널에서 yarn add @reduxjs/toolkit 또는 npm install @reduxjs/toolkit 명령어르 실행한다.

 

파일 구조

  • src/store/store.js

store를 만든다.

import { configureStore } from '@reduxjs/toolkit'
import { persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import rootReducer from './rootReducer'

const persistConfig = {
  key: 'root',
  storage
}

const reducer = persistReducer(persistConfig, rootReducer)

const store = configureStore({
  reducer
})

export default store

 

  • src/store/rootReducer.js

rootRedecer를 만든다.

toDos 이외의 reducer를 만들게 되면 rootReducer에 추가한다.

import { combineReducers } from 'redux'
import toDos from './toDoReducer'

const rootReducer = combineReducers({
  toDos
})

export default rootReducer

 

  • src/store/toDoReducer.js

reducers 안에 액션을 만든다.

initialState의 toDoList에 toDo를 추가하고, 삭제하는 기능을 만들기 위해 addToDo와 deleteToDo를 만든다.

addToDo 액션에서 unshift()를 통해 배열에 새로운 toDo를 추가하고,

deleteToDo 액션에서 filter()를 통해 id값이 일치하지 않는 나머지 값들만 return해 toDo를 삭제한다.

import { createSlice } from '@reduxjs/toolkit'

const todoSlice = createSlice({
  name: 'reducerToDo',

  initialState: {
    toDoList: []
  },

  reducers: {
    addToDo: (state, action) => {
      state.toDoList.unshift({ id: Date.now(), text: action.payload })
    },
    deleteToDo: (state, action) => {
      state.toDoList = state.toDoList.filter(list => list.id !== action.payload)
    }
  }
})

export const { addToDo, deleteToDo } = todoSlice.actions
export default todoSlice.reducer

 

  • src/routes/Home.js

useSelector()로 스토어에 있는 todoList state를 가지고 온다.

dispatch를 통해 reducers에 만들어 놓은 액션을 가지고 온다.

import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { addToDo } from '../store/store'
import ToDo from '../components/ToDo'
import './home.scss'

function Home() {
  // ** Hooks
  const dispatch = useDispatch()

  // ** States
  const [text, setText] = useState('')

  // ** Redux States
  const toDos = useSelector(state => state.todoList)

  function onChange(e) {
    setText(e.target.value)
  }

  function onSubmit(e) {
    e.preventDefault()
    dispatch(addToDo(text))
    setText('')
  }

  return (
    <>
      <div className="layout home">
        <h1>To Do List</h1>

        <form onSubmit={onSubmit}>
          <input type="text" value={text} onChange={onChange} placeholder="What is your to do?"/>
          <button>Add</button>
        </form>

        <ul>
          {
            toDos.map((toDo, index) => {
              return (
                <ToDo id={toDo.id} text={toDo.text} key={index}/>
              )
            })
          }
        </ul>
      </div>
    </>
  )
}

export default Home

버전에 맞춰서 강의와 다른 코드가 있습니다.

강의와 별개로 추가한 내용입니다.

 

결과 화면

 


scss를 사용하기 위해 터미널에서 yarn add node-sass 또는 npm install node-sass 명령어를 실행한다.

 

파일 구조

  • src/scss/layout/_layout.scss
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&display=swap');

body {
    background-color: burlywood;
    font-family: 'Noto Sans', 'Noto Sans KR', sans-serif;

    .layout {
        position: relative;
        top: 0;
        left: 0;
        bottom: 0;
        right: 0;
        max-width: 500px;
        width: 90%;
        margin: 50px auto;
        padding: 30px;
        background-color: bisque;

        button {
            min-height: 30px;
            padding: 0 10px;
            background-color: #fff;
        }
    }
}

 

  • src/routes/home.scss
.home {
    h1 {
        margin-bottom: 30px;
        text-align: center;
    }

    form {
        display: flex;
        justify-content: space-between;
        gap: 10px;
        margin-bottom: 10px;

        input {
            width: 100%;
            height: 30px;
            padding: 10px;
        }
    }
}

 

  • src/components/todo.scss
ul {
    li {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 10px;

        &:last-child {
            margin-bottom: 0;
        }
    }
}

 

  • src/routes/detail.scss
.detail {
    h1 {
        margin-bottom: 30px;
        text-align: center;
    }

    .date {
        margin-bottom: 10px;
        text-align: right;
    }
}

버전에 맞춰서 강의와 다른 코드가 있습니다.

 

결과 화면


파일 구조

  • src/components/ToDo.js

state의 id값을 이용해서 상세페이지로 보내준다.

import React from 'react'
import { Link } from 'react-router-dom'
import { useDispatch } from 'react-redux'
import { deleteToDo } from '../store/store'

function ToDo(props) {
  // ** Hooks
  const dispatch = useDispatch()

  function onDelete(e) {
    e.preventDefault()
    dispatch(deleteToDo(props.id))
  }

  return (
    <li>
      <Link to={`/${props.id}`}>
        <p>{ props.text }</p>
      </Link>
      <button onClick={onDelete}>DEL</button>
    </li>
  )
}

export default ToDo

 

  • src/routes/Detail.js

useParams 훅을 사용해서 url의 파라미터값을 가지고 온다.

useSelector를 사용해 state를 가지고 온다.

state의 id값과 파라미터의 id값을 비교해 해당 리스트를 가지고 온다. (파라미터의 타입이 String이기 때문에 Number로 타입을 변환해 값을 비교한다.)

import React from 'react'
import { useParams } from 'react-router-dom'
import { useSelector } from 'react-redux'

function Detail() {
  // ** Hooks
  const param = useParams()

  // ** Redux States
  const toDos = useSelector(state => state)

  const toDo = toDos.find(list => list.id === Number(param.id))

  return (
    <>
      <h1>Detail</h1>

      <p>Created: {toDo.id}</p>
      <p>{toDo.text}</p>
    </>
  )
}

export default Detail

버전에 맞춰서 강의와 다른 코드가 있습니다.

 

결과 화면


파일 구조

  • src/components/ToDo.js

To Do List 추가하기 코드에서 dispatch, onDelete() 함수와 클릭 이벤트를 추가했다.

import React from 'react'
import { useDispatch } from 'react-redux'
import { deleteToDo } from '../store/store'

function ToDo(props) {
  // ** Hooks
  const dispatch = useDispatch()

  function onDelete(e) {
    e.preventDefault()
    dispatch(deleteToDo(props.id))
  }

  return (
    <li>
      { props.text } <button onClick={onDelete}>DEL</button>
    </li>
  )
}

export default ToDo

 

  • src/routes/Home.js

To Do List 추가하기 코드에서 ToDo 컴포넌트에 id 값이 추가됐다.

import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { addToDo } from '../store/store'
import ToDo from '../components/ToDo'

function Home() {
  // ** Hooks
  const dispatch = useDispatch()

  // ** States
  const [text, setText] = useState('')

  // ** Redux States
  const toDos = useSelector(state => state)

  function onChange(e) {
    setText(e.target.value)
  }

  function onSubmit(e) {
    e.preventDefault()
    dispatch(addToDo(text))
    setText('')
  }

  return (
    <>
      <h1>To Do</h1>

      <form onSubmit={onSubmit}>
        <input type="text" value={text} onChange={onChange}/>
        <button>Add</button>
      </form>

      <ul>
        {
          toDos.map((toDo, index) => {
            return (
              <ToDo id={toDo.id} text={toDo.text} key={index}/>
            )
          })
        }
      </ul>
    </>
  )
}

export default Home

버전에 맞춰서 강의와 다른 코드가 있습니다.

 

결과 화면


파일 구조

  • src/index.js

Provider는 <App/> 컴포넌트가 리덕스의 store에 접근할 수 있도록 한다.

import React from 'react'
import ReactDOM from 'react-dom/client'
import {Provider} from 'react-redux'
import store from './store/store'
import App from './App'

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

 

  • src/components/ToDo.js

<ToDo/> 컴포넌트를 만들어 props로 데이터를 전달한다.

import React from 'react'

function ToDo(props) {
  return (
    <li>
      { props.text }
    </li>
  )
}

export default ToDo

 

  • src/routes/Home.js

useSelector를 사용해 store의 state를 가지고 온다.

import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { addToDo } from '../store/store'
import ToDo from '../components/ToDo'

function Home() {
  // ** Hooks
  const dispatch = useDispatch()

  // ** States
  const [text, setText] = useState('')

  // ** Redux States
  const toDos = useSelector(state => state)

  function onChange(e) {
    setText(e.target.value)
  }

  function onSubmit(e) {
    e.preventDefault()
    dispatch(addToDo(text))
    setText('')
  }

  return (
    <>
      <h1>To Do</h1>

      <form onSubmit={onSubmit}>
        <input type="text" value={text} onChange={onChange}/>
        <button>Add</button>
      </form>

      <ul>
        {
            toDos.map((toDo, index) => {
              return (
                <ToDo text={toDo.text} key={index}/>
              )
            })
         }
      </ul>
    </>
  )
}

export default Home

결과 화면


버전

react: ^18.2.0

react-dom: ^18.2.0

react-redux: ^8.0.5

react-router-dom: ^6.10.0

react-scripts: 5.0.1

redux: ^4.2.1

 

위의 버전에 맞춰서 강의와 다른 코드가 있습니다.

 

파일 구조

  • src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

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

 

  • src/App.js
import React from 'react'
import {BrowserRouter, Routes, Route} from 'react-router-dom'
import Home from './routes/Home'
import Detail from './routes/Detail'

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home/>}></Route>
        <Route path="/:id" element={<Detail/>}></Route>
      </Routes>
    </BrowserRouter>
  )
}

export default App

 

  • src/store/store.js
import { createStore } from 'redux'

const ADD = 'ADD'
const DELETE = 'DELETE'

export const addToDo = text => {
  return {
    type: ADD,
    text
  }
}

export const deleteToDo = id => {
  return {
    type: DELETE,
    id
  }
}

const reducer = (state = [], action) => {
  switch (action.type) {
    case ADD:
      return [{ id: Date.now(), text: action.text}, ...state]
    case DELETE:
      return state.filter(toDo => toDo.id !== action.id)
    default:
      return state
  }
}

const store = createStore(reducer)

export default store

 

  • src/routes/Home.js
import React, { useState } from 'react'

function Home() {
  const [text, setText] = useState('')

  function onChange(e) {
    setText(e.target.value)
  }

  function onSubmit(e) {
    e.preventDefault()
    setText('')
    console.log(text)
  }

  return (
    <>
      <h1>To Do</h1>

      <form onSubmit={onSubmit}>
        <input type="text" value={text} onChange={onChange}/>
        <button>Add</button>
      </form>

      <ul></ul>
    </>
  )
}

export default Home

 

  • src/routes/Detai.js
export default () => 'Detail'

결과 화면


1. 바닐라 자바스크립트로 투두 리스트 만들기

  • index.html
<body>
    <h1>To dos</h1>

    <form>
        <input type="text">
        <button>Confirm</button>
    </form>

    <ul></ul>
</body>

 

  • index.js
// html 태그 선택
const form = document.querySelector('form')
const input = document.querySelector('input')
const ul = document.querySelector('ul')

// li에 input의 value 값을 넣어 화면에 보여준다.
const createToDo = toDo => {
  const li = document.createElement('li')
  li.innerText = toDo
  ul.appendChild(li)
}

// 폼을 제출하면 input을 비우고, createToDo 함수를 실행한다.
const onSubmit = e => {
  e.preventDefault()
  const toDo = input.value
  input.value = ''
  createToDo(toDo)
}

form.addEventListener('submit', onSubmit)

 

문제점

데이터를 저장하는 것이 아니라 단순히 innerText를 통해 input.value의 내용을 화면에 보여주는 것이다.

새로고침 시, 데이터가 날아간다.

 


2. 리덕스로 투두 리스트 만들기

  • index.html
<body>
    <h1>To dos</h1>

    <form>
        <input type="text">
        <button>Confirm</button>
    </form>

    <ul></ul>
</body>

 

  • index.js

state는 read-only 이기 때문에 직접 state를 수정하면 안 된다.

store의 state를 수정할 수 있는 유일한 방법은 action을 보내는 것이다.

state를 mutate 하는 것이 아니라, 새로운 state를 return 해야 한다. ex) push, pop을 통해서 배열을 직접 수정하면 안 된다.

import {createStore} from 'redux'

// html 태그 선택
const form = document.querySelector('form')
const input = document.querySelector('input')
const ul = document.querySelector('ul')

const ADD_TODO = "ADD_TODO"
const DELETE_TODO = "DELETE_TODO"

// 2. 리듀서 만들기
const reducer = (state = [], action) => {
  // 3. 액션 만들기
  switch(action.type) {
    case ADD_TODO:
      return [{ id: Date.now(), text: action.text }, ...state] // state.push(action.text) X
    case DELETE_TODO:
      return state.filter(toDo => toDo.id !== parseInt(action.id)) // filter(): 조건에 만족하는 리스트들을 새로운 배열로 return
    default:
      return state
  }
}

// 1. 스토어 만들기
const store = createStore(reducer)

// 투두 리스트 추가하기
const addToDo = text => {
  return {
    type: ADD_TODO,
    text
  }
}

// 투두 리스트 삭제하기
const deleteToDo = id => {
  return {
    type: DELETE_TODO,
    id
  }
}

// 4. 디스페치 만들기
const dispatchAddToDo = text => {
  store.dispatch(addToDo(text))
}

const dispatchDeleteToDo = e => {
  const id = parseInt(e.target.parentNode.id)
  store.dispatch(deleteToDo(id))
}

// 5. 이벤트 함수 만들기
const paintToDos = () => {
  const toDos = store.getState()

  // ul을 빈 값으로 만든 다음에 li를 추가시킨다. (새로운 return 값을 등록해야 하기 때문에 기존에 등록된 내용을 비운다.)
  ul.innerHTML = ''

  toDos.forEach(toDo => {
    const li = document.createElement('li')
    const btn = document.createElement('button')

    btn.innerText = 'DEL'
    btn.addEventListener('click', dispatchDeleteToDo) // 디스패치 실행

    li.id = toDo.id
    li.innerText = toDo.text

    li.appendChild(btn)
    ul.appendChild(li)
  })
}

// paintToDos의 변화를 감지한다.
store.subscribe(paintToDos)

// 5. 이벤트 함수 만들기
const onSubmit = e => {
  e.preventDefault()
  const toDo = input.value
  input.value = ''
  dispatchAddToDo(toDo) // 디스패치 실행
}

form.addEventListener('submit', onSubmit)

결과 화면

Plus 버튼을 누르면 숫자가 1 증가하고, Minus 버튼을 누르면 숫자가 1 감소한다.


1. 바닐라 자바스크립트로 카운터 만들기

  • index.html
<body>
    <button id="minus">Minus</button>
    <span id="number"></span>
    <button id="plus">Plus</button>
</body>

 

  • index.js
// html 태그 선택
const plus = document.getElementById('plus')
const minus = document.getElementById('minus')
const number = document.getElementById('number')

// 초기값
let count = 0

// html 화면에 초기값 count 보여주기
number.innerText = count

// count가 변할 때 마다 화면에 보여주기
const updateText = () => {
  number.innerText = count
}

// count +1
const handlePlus = () => {
  count++
  updateText()
}

// count -1
const handleMinus = () => {
  count--
  updateText()
}

// 버튼 클릭 이벤트
plus.addEventListener('click', handlePlus)
minus.addEventListener('click', handleMinus)

 


※ 리덕스 기본 개념

  • store

data를 저장하는 곳이다.

import { createStore } from 'redux'
const store = createStore(reducer)

 

  • state(상태)

변경이 필요한 data이다.

 

  • reducre

state를 변경하는 function(함수)이다.

reducer의 return 값이 state가 된다.

const reducer = (state, action) => {}

 

  • action

reducer의 두번째 파라미터 값이다.

action을 통해서만 state를 변경할 수 있다.

action은 object(객체)타입만 가능하고, 그 key 값은 type만 가능하다. (action.type)

공식문서에 따르면 if문 보다는 switch문이랑 더 자주 사용된다.

 

  • dispatch

reducer에 action을 보내는 방법이다.

dispatch를 통해서만 action을 실행시킬 수 있다.

현재 이벤트를 발생시키는 함수라고 볼 수 있다.

store.dispatch({type: 액션이름})

 

  • subscribe

store 안에 있는 변화를 감지한다.

store.subscribe(함수호출)

 


2. 리덕스로 카운터 만들기

yarn add redux 또는 npm install redux

 

  • index.html
<body>
    <button id="plus">Plus</button>
    <span id="number"></span>
    <button id="minus">Minus</button>
</body>

 

  • index.js
import { createStore } from 'redux'

// html 태그 선택
const plus = document.getElementById('plus')
const minus = document.getElementById('minus')
const number = document.getElementById('number')

// 초기값
number.innerText = 0

// string을 직접 사용하기 보다는 변수로 만들어서 사용하는 것이 오타 등의 에러를 발견할 수 있어 효율적이다.
const ADD = 'ADD'
const MINUS = 'MINUS'

// 2. reducer(변수 이름은 자유)를 만든다.
const reduceCountModifier = (count = 0, action) => {
  // 3. action을 만든다.
  switch (action.type) {
    case ADD:
      return count + 1
    case MINUS:
      return count -1
    default:
      return count
  }
}

// 1. store를 만든다.
const countStore = createStore(reduceCountModifier)

// store 안에 있는 state를 가지고 온다.
const onChange = () => {
  number.innerText = countStore.getState()
}

// store 안에 있는 state의 변화를 감지한다.
countStore.subscribe(onChange)

// 4. dispatch를 만든다.
const handlePlus = () => {
  countStore.dispatch({type: ADD})
}
const handleMinus = () => {
  countStore.dispatch({type: MINUS})
}

// 클릭 이벤트
plus.addEventListener('click', handlePlus)
minus.addEventListener('click', handleMinus)

리덕스는 자바스크립트 앱을 위한 예측 가능한 상태(state) 컨테이너이다.

리덕스는 자바스크립트를 사용할 수 있다면 바닐라 자바스크립트, 리액트, 뷰, 앵귤러 등 여러 환경에서 함께 사용할 수 있다.

(리액트와 많이 사용하긴 하지만, 리액트를 위한 전용 라이브러리가 아니다.)

 

1. 깃허브 레포지토리 만들기

https://github.com/heejae0811/vanilla-redux

 

2. npx create-react-app vanilla-redux(레포이름)

 

3. 불필요한 파일 삭제하기

+ Recent posts