목표

  • 생성, 수정, 삭제 페이지에 직접 작성한 axios 코드들을 Redux Toolkit을 통해 동작하게 만든다.

 


결과 화면

 


1. src/store/boardReducer.tsx

https://redux-toolkit.js.org/api/createAsyncThunk

createAsyncThunk에 대한 자세한 설명은 위의 공식 문서를 참고한다.

 

 

Redux Toolkit에서 axios와 같은 비동기 처리를 하기 위해서는 createAsyncThunk를 사용한다.

또한 비동기 처리는 일반 reducers가 아니라 extraReducers에 액션 타입을 작성하고,

비동기 요청의 수명 주기를 나타내는 pending(대기), fulfilled(이행), rejected(실패) 작업을 처리할 수 있다.

 

  • postBoardList (게시판 생성 페이지)

postBoardList는 입력한 데이터를 서버에 보내야 하기 때문에 POST 메서드를 사용하고, boardList 파라미터를 통해 입력한 값을 보낸다.

boardList 파마미터는 타입을 정의하지 않으면 오류가 발생하기 때문에 interface로 타입을 정의한다.

또한 POST로 값을 보내면 되기 때문에 값을 return 할 필요 없다.

extraReducers에서 fulfilled(이행) 상태일 때, postBoardList action 타입을 return 한다.

import axios from 'axios'
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'

interface Iboard {
  id?: number,
  title: string,
  content: string,
  created_at?: Date,
  updated_at?: Date
}

const initialState: any = {}

export const postBoardList = createAsyncThunk(
  'POST_BOARD_LIST',
  async (boardList: Iboard) => {
    try {
      await axios.post('http://localhost:3001/board', boardList)
    } catch (error) {
      console.log(error)
    }
  }
)

export const boardSlice = createSlice({
  name: 'boardList',

  initialState,

  reducers: {},

  extraReducers: (builder) => {
    // POST BOARD LIST
    builder.addCase(postBoardList.fulfilled, (state, action) => {
      return action.payload
    })
  }
})

 

  • postBoardList (게시판 수정 페이지)

patchBoardList는 수정한 데이터를 서버에 보내야 하기 때문에 PATCH 메서드를 사용하고, boardList 파라미터를 통해 수정한 값을 보낸다.

boardList 파마미터 타입을 정의하지 않으면 오류가 발생해 interface로 타입을 정의한다.

존재하지 않는 파라미터 값을 요청할 경우, 에러 페이지로 갈 수 있게 처리한다.

또한 PATCH로 값을 보내면 되기 때문에 값을 return 할 필요 없다.

extraReducers에서 fulfilled(이행) 상태일 때, patchBoardList action 타입을 return 한다.

import axios from 'axios'
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'

interface Iboard {
  id?: number,
  title: string,
  content: string,
  created_at?: Date,
  updated_at?: Date
}

const initialState: any = {}

export const patchBoardList = createAsyncThunk(
  'PATCH_BOARD_LIST',
  async (boardList: Iboard) => {
    try {
      await axios.patch(`http://localhost:3001/board/${boardList.id}`, boardList)
    } catch (error: any) {
      if(error.response.status === 404) {
        window.location.href='/error/404'
      }
      console.log(error)
    }
  }
)

export const boardSlice = createSlice({
  name: 'boardList',

  initialState,

  reducers: {},

  extraReducers: (builder) => {
    // PATCH BOARD LIST
    builder.addCase(patchBoardList.fulfilled, (state, action) => {
      return action.payload
    })
  }
})

 

  • deleteBoardList (게시판 삭제 페이지)

deleteBoardList는 서버 데이터를 삭제해야 하기 때문에 DELETE 메서드를 사용하고, 파라미터에 해당하는 데이터만 삭제해야 하기 때문에 파라미터 값을 보낸다.

또한 DELETE로 값을 삭제하면 되기 때문에 값을 return 할 필요 없다.

extraReducers에서 fulfilled(이행) 상태일 때, deleteBoardList action 타입을 return 한다.

import axios from 'axios'
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'

interface Iboard {
  id?: number,
  title: string,
  content: string,
  created_at?: Date,
  updated_at?: Date
}

const initialState: any = {}

export const deleteBoardList = createAsyncThunk(
  'DELETE_BOARD_LIST',
  async (params: string | undefined) => {
    try {
      await axios.delete(`http://localhost:3001/board/${params}`)
    } catch (error) {
      console.log(error)
    }
  }
)

export const boardSlice = createSlice({
  name: 'boardList',

  initialState,

  reducers: {},

  extraReducers: (builder) => {
    // DELETE BOARD LIST
    builder.addCase(deleteBoardList.fulfilled, (state, action) => {
      return action.payload
    })
  }
})

 

2. 게시판 생성 페이지

  • src/pages/board/create/index.tsx

dispatch()를 통해 store에 만든 postBoardList()를 실행하고, [title], [content] state 변수를 boardList 객체로 만들어 파라미터 값으로 넘긴다.

import { useAppDispatch } from '../../../hooks/useApp'
import { postBoardList } from '../../../store/boardReducer'
...

const BoardCreate = () => {
  // ** Hooks
  const dispatch = useAppDispatch()
  const navigate = useNavigate()

  // ** States
  let [title, setTitle] = useState<string>('')
  let [content, setContent] = useState<string>('')

  const boardSubmit = (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault()

    const boardList = {
      title: title,
      content: content
    }
    dispatch(postBoardList(boardList))
    
    ...
  }

  ...

  return (
    <div className="board-create">
      <form>
        <div>
          <Label htmlFor="title">Title</Label>
          <Input type="text" id="title" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="제목을 입력해주세요."/>
        </div>

        <div>
          <Label htmlFor="content">Content</Label>
          <Textarea name="content" id="content" value={content} onChange={(e) => setContent(e.target.value)} placeholder="내용을 입력해주세요."/>
        </div>
      </form>

      <div className="grid-2">
        <Button children="Confirm" variant="primary" onClick={boardSubmit}/>
        <Button children="Cancel" variant="secondary" onClick={boardCancel}/>
      </div>
    </div>
  )
}

export default BoardCreate

 

3. 게시판 수정, 삭제 페이지

  • src/pages/board/modify/index.tsx

수정 페이지는 등록된 값들을 수정해야 하기 때문에 dispatch()를 통해 getBoardDetail()에 파라미터 값을 넘겨 파라미터에 해당하는 값을 가져온다.

이 값을 [title], [content] state 변수의 초깃값으로 만들고, boardList 객체로 만들어 파라미터 값으로 넘긴다.

 

삭제 기능은 dispatch()를 통해 deleteBoardList()에 파라미터 값을 넘겨 파라미터에 해당하는 값을 삭제한다.

import { useAppDispatch, useAppSelector } from '../../../hooks/useApp'
import { getBoardDetail, deleteBoardList, patchBoardList } from '../../../store/boardReducer'
...

const BoardModify = () => {
  // ** Hooks
  const dispatch = useAppDispatch()
  const params = useParams().id

  // ** Redux States
  const boardDetail = useAppSelector(state => state.boardReducer)

  // ** States
  let [title, setTitle] = useState<string>(boardDetail.title)
  let [content, setContent] = useState<string>(boardDetail.content)

  const boardDelete= (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault()
    
    dispatch(deleteBoardList(params))
    
    ...
  }

  const boardSubmit = (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault()

    const boardList = {
      id: boardDetail.id,
      title: title,
      content: content,
      updated_at: new Date()
    }
    dispatch(patchBoardList(boardList))
    
    ...
    }
  }

  useEffect(() => {
    dispatch(getBoardDetail(params))
  }, [])
  
  ...

  return (
    <div className="board-modify">
      <form>
        <div>
          <Label htmlFor="title">Title</Label>
          <Input type="text" id="title" value={title || ""} onChange={(e) => setTitle(e.target.value)} placeholder="제목을 입력해주세요."/>
        </div>

        <div>
          <Label htmlFor="content">Content</Label>
          <Textarea name="content" id="content" value={content || ""} onChange={(e) => setContent(e.target.value)} placeholder="내용을 입력해주세요."/>
        </div>
      </form>

      <div className="grid-3">
        <Button children="Delete" variant="danger" onClick={boardDelete}/>
        <Button children="Confirm" variant="primary" onClick={boardSubmit}/>
        <Button children="Cancel" variant="secondary" onClick={boardCancel}/>
      </div>
    </div>
  )
}

export default BoardModify

 

#6.2
createAsyncThunk와 extraReducers를 통해
생성, 수정, 삭제 페이지를 만들었다.

목표

  • 목록, 상세 페이지에 직접 작성한 axios 코드들을 Redux Toolkit을 통해 동작하게 만든다.

 


결과 화면

 


1. src/store/boardReducer.tsx

https://redux-toolkit.js.org/api/createAsyncThunk

createAsyncThunk에 대한 자세한 설명은 위의 공식 문서를 참고한다.

 

 

Redux Toolkit에서 axios와 같은 비동기 처리를 하기 위해서는 createAsyncThunk를 사용한다.

또한 비동기 처리는 일반 reducers가 아니라 extraReducers에 액션 타입을 작성하고,

비동기 요청의 수명 주기를 나타내는 pending(대기), fulfilled(이행), rejected(실패) 작업을 처리할 수 있다.

 

  • getBoardList (게시판 목록 페이지)

getBoardList는 게시판 목록 데이터를 서버에서 가져와야 하기 때문에 GET 메서드를 사용하고, 화면에 값을 출력해야 하기 때문에 값을 return 한다.

extraReducers에서 fulfilled(이행) 상태일 때, getBoardList action 타입을 return 한다.

import axios from 'axios'
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'

const initialState: any = {}

export const getBoardList = createAsyncThunk(
  'GET_BOARD_LIST',
  async () => {
    try {
      const response = await axios.get('http://localhost:3001/board')
      return response.data
    } catch (error) {
      console.log(error)
    }
  }
)

export const boardSlice = createSlice({
  name: 'boardList',

  initialState,

  reducers: {},

  extraReducers: (builder) => {
    // GET BOARD LIST
    builder.addCase(getBoardList.fulfilled, (state, action) => {
      return action.payload
    })
  }
})

 

  • getBoardDetail (게시판 상세 페이지)

getBoardDetail은 게시판 상세 데이터를 서버에서 가져와야 하기 때문에 GET 메서드를 사용하고, 화면에 값을 출력해야 하기 때문에 값을 return 한다.

상세 페이지 1개의 데이터만 필요하기 때문에 파라미터 값을 넘겨주고, 존재하지 않는 파라미터 값을 요청할 경우, 에러 페이지로 갈 수 있게 처리한다.

extraReducers에서 fulfilled(이행) 상태일 때, getBoardDetail action 타입을 return 한다.

import axios from 'axios'
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'

const initialState: any = {}

export const getBoardDetail = createAsyncThunk(
  'GET_BOARD_DETAIL',
  async (params: string | undefined) => {
    try {
      const response = await axios.get(`http://localhost:3001/board/${params}`)
      return response.data
    } catch (error: any) {
      if(error.response.status === 404) {
        window.location.href='/error/404'
      }
      console.log(error)
    }
  }
)

export const boardSlice = createSlice({
  name: 'boardList',

  initialState,

  reducers: {},

  extraReducers: (builder) => {
    // GET BOARD DETAIL
    builder.addCase(getBoardDetail.fulfilled, (state, action) => {
      return action.payload
    })
  }
})

 

2. 게시판 목록 페이지

  • src/pages/board/list/index.tsx

dispatch()를 통해 store에 만든 getBoardList() 목록 데이터를 가져온다.

가져온 데이터를 useAppSelector()를 통해 state에 저장한다.

state 값을 화면에 출력한다.

import { useAppDispatch, useAppSelector } from '../../../hooks/useApp'
import { getBoardList } from '../../../store/boardReducer'
import { boardType } from '../../../type/boardType'
...

const BoardList = () => {
  // ** Hooks
  const dispatch = useAppDispatch()

  // ** Redux States
  const boardList = useAppSelector(state => state.boardReducer)
  
  // ** States
  const [currentPost, setCurrentPost] = useState<boardType[]>(boardList)

  useEffect(() => {
    dispatch(getBoardList())
  }, [])
  
  ...

  return (
    <div className="board-list">
      ...
      
      <table>
        <tbody>
          {
            currentPost.length > 0 && currentPost.map((board, index) => {
              return (
                <tr key={index}>
                  <td>{index + 1}</td>
                  <td className="title">
                    <Link to={`/board/${board.id}`}>{board.title}</Link>
                  </td>
                  <td>{dayjs(board.created_at).format('YYYY.MM.DD')}</td>
                </tr>
              )
            })
          }
        </tbody>
      </table>
      
      ...
    </div>
  )
}

export default BoardList

 

3. 게시판 상세 페이지

  • src/pages/board/detail/index.tsx

dispatch()를 통해 sotre에 만든 getBoardDetail() 상세 데이터를 가져온다. 이때, 파라미터 값을 넘겨 파라미터에 해당하는 상세 데이터만 가져온다.

가져온 데이터를 useAppSelector()를 통해 state에 저장한다.

state 값을 화면에 출력한다.

import { useAppDispatch, useAppSelector } from '../../../hooks/useApp'
import { getBoardDetail } from '../../../store/boardReducer'
...

const BoardDetail = () => {
  // ** Hooks
  const dispatch = useAppDispatch()
  const params = useParams().id

  // ** Redux States
  const boardDetail = useAppSelector(state => state.boardReducer)

  useEffect(() => {
    dispatch(getBoardDetail(params))
  }, [])

  return (
    <div className="board-detail">
      <Title children={boardDetail.title}/>

      <div className="board-wrapper">
        <div className="board-header">
          <p>No.{boardDetail.id}</p>
          <p>{dayjs(boardDetail.created_at).format('YYYY.MM.DD')}</p>
        </div>

        <div className="board-content">
          <p>{boardDetail.content}</p>
        </div>
      </div>
    </div>
  )
}

export default BoardDetail

 

#6.2
createAsyncThunk와 extraReducers를 통해
목록 페이지와 상세페이지를 만들었다.

목표

  • React Redux TypeScript 세팅하기
  • Redux Toolkit TypeScript 세팅하기

Redux Toolkit 패키지는 Redux를 작성하는 표준 방법이다.

Reat Redux를 쉽게 사용하기 위해서 Redux Toolkit을 함께 사용한다.

 


https://react-redux.js.org/introduction/getting-started

https://redux-toolkit.js.org/introduction/getting-started

아래 코드들은 공식 문서 기반으로 작성된 코드이다.

React Redux, Redux Toolkit에 대한 자세한 설명은 위의 공식 문서를 참고한다.

 

폴더구조

 


1. react-redux, @reduxjs/toolkit 패키지 설치하기

터미널에서 아래 명령어를 실행한다.

package.json 파일에서 설치된 버전을 확인할 수 있다.

yarn add react-redux @reduxjs/toolkit
또는
npm install react-redux @reduxjs/toolkit

 

2. store 만들기

  • src/store/store.tsx

타입스크립트이기 때문에 store에도 타입을 정의한다.

import { configureStore } from '@reduxjs/toolkit'
import { boardSlice } from './boardReducer'

export const store = configureStore({
  reducer: {
    boardReducer: boardSlice.reducer
  }
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

 

3. Provider 컴포넌트 사용하기

  • src/index.tsx

store를 사용하기 위해서 <App/> 컴포넌트를 <Provider> 컴포넌트로 감싼다.

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') as HTMLElement
)

root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App/>
    </Provider>
  </React.StrictMode>
)

 

4. reducer 만들기

  • src/store/boardReducer.tsx

게시판에 관련된 코드들을 reducer로 동작하게 만들 것이다.

import { createSlice } from '@reduxjs/toolkit'

const initialState: any = {}

export const boardSlice = createSlice({
  name: 'boardList',

  initialState,

  reducers: {},

  extraReducers: (builder) => {}
})

 

5. useDispatch, useSelector 타입 정의하기

  • src/hooks/useApp.tsx

타입스크립트에서 useDispatch와 useSelector은 타입을 정의하지 않으면 오류가 발생하기 때문에 hook으로 만들어 사용한다.

import { useDispatch, useSelector } from 'react-redux'
import type { TypedUseSelectorHook } from 'react-redux'
import type { AppDispatch, RootState } from '../store/store'

export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

 

사용할 페이지에서 useDispatch와 useSelector를 import 해 사용한다.

import { useAppDispatch, useAppSelector } from '../../../hooks/useApp'
import { getBoardList } from '../../../store/boardReducer'
...

const BoardList = () => {
  // ** Hooks
  const dispatch = useAppDispatch()

  // ** Redux States
  const boardList = useAppSelector(state => state.boardReducer)

  useEffect(() => {
    dispatch(getBoardList())
  }, [])

  ...
  
  return (
    <>
      ...
    </>
  )
}

export default BoardList

 

#6.1
React Redux와 Rudux Toolkit 세팅이 완료됐다.
기존에 만든 게시판 코드들을 Redux Toolkit을 사용해서 동작하게 만들 것이다.

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

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

 

결과 화면

 


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'

+ Recent posts