목표

  • 생성, 수정, 삭제 페이지에 직접 작성한 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을 사용해서 동작하게 만들 것이다.

+ Recent posts