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

 

결과 화면


파일 구조

  • 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