결과 화면

 


수정사항

https://jae-study.tistory.com/85

기존에는 react-js-pagination 패키지를 사용해 페이지네이션을 구현했다.

하지만 페이지네이션을 프론트에서 만들었을 경우, 서버와 데이터베이스의 부하가 발생할 수 있기 때문에 백앤드의 작업이 필요하다.

백앤드 없이 프론트에서 페이지네이션을 구현하고 싶다면 위의 링크를 참고한다.

 

https://jae-study.tistory.com/138

NestJs로 페이지네이션을 만들었다.

페이지네이션을 백앤드에서 만들어야 하는 이유에 대한 설명은 위의 링크를 참고한다.

 


1. boardReducer.tsx

  • src/store/boardReducer.tsx
  1. page 파라미터를 받는다. 타입스크립트이기 때문에 타입을 정의한다.
  2. axios.get을 사용해 page에 해당하는 데이터를 요청한다. URL은 쿼리 파라미터가 붙은 key=value 형태이다.
export const getBoardList = createAsyncThunk(
  'GET_BOARD_LIST',
  async (page: number) => {
    try {
      const response = await axios.get(`http://localhost:3001/board?page=${page}`)
      return response.data
    } catch (error) {
      console.log(error)
    }
  }
)

...

 

2. 페이지네이션에 해당하는 데이터 가져오기

  • src/pages/board/list/index.tsx
  1. dispatch를 사용해 page에 해당하는 게시판 데이터를 가져온다.
  2. axios를 통해 받아온 데이터가 객채(object) 형태이기 때문에 (배열처럼 보이지만 typeof로 확인하면 object이다.) [board, setBoard] state 변수를 만들고, 전개 연산자를 사용해 배열 형태로 바꾼다.
  3. axios를 통해 받아온 page 데이터가 문자(string) 형태이기 때문에 [page, setPage] state 변수를 만들고, Number로 타입을 변환한다.
  4. 페이지네이션 버튼에 대한 공통 함수(handlePagination)를 만든다.
import { useEffect, useState } from 'react'
import { useAppDispatch, useAppSelector } from '../../../hooks/useApp'
...

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

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

  // ** States
  const [board, setBoard] = useState<boardType[]>([])
  const [page, setPage] = useState<number>(1)

  // 페이지네이션
  function handlePagination(newPage: number) {
    if(newPage >= 1 && newPage <= boardList.meta.last_page) {
      dispatch(getBoardList(newPage))
    }
  }

  // 페이지가 로딩되면 게시판 리스트 가져옴
  useEffect(() => {
    dispatch(getBoardList(page))
  }, [])

  // 페이지가 로딩된 후 state 변수에 boardList 데이터 저장함
  useEffect(() => {
    if (boardList && boardList.data) {
      setBoard([...boardList.data])
      setPage(Number(boardList.meta.page))
    }
  }, [boardList])

  return (
    ...
  )
}

export default BoardList

 

3. 마크업 하기

  • src/pages/board/list/index.tsx.
  1. 삼항연산자를 사용해 데이터가 있고, 없을 때 마크업을 다르게 표현한다.
  2. map 메서드를 사용해 게시판 데이터를 보여준다.
  3. 처음으로, 이전, 다음, 마지막으로 버튼 기능을 구현한다.
const BoardList = () => {
  ...

  return (
    <div className="board-list">
      <Title children="Board list"/>

      <h4>Total post : {boardList.meta?.total}</h4>

      <table>
        <colgroup>
          <col width="15%"/>
          <col width="65%"/>
          <col width="20%"/>
        </colgroup>

        <thead>
          <tr>
            <th>No</th>
            <th>Title</th>
            <th>Date</th>
          </tr>
        </thead>

        <tbody>
          {
            board.length === 0 ? (
              <tr>
                <td colSpan={3}>데이터가 없습니다.</td>
              </tr>
            ) : (
              board.map((board, index) => {
                return (
                  <tr key={index}>
                    <td>{board.id}</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>

      {
        board.length === 0 ? (
          <></>
        ) : (
          <div className="pagination">
            {
              boardList.meta.last_page === 1 ? (
                <>
                  <button disabled>&#60;</button>
                  <p>{page}</p>
                  <button disabled>&#62;</button>
                </>
              ) : (
                <>
                  <button onClick={() => handlePagination(1)}>&#60;&#60;</button>
                  <button onClick={() => handlePagination(page - 1)}>&#60;</button>
                  <p>{page} / {boardList.meta.last_page}</p>
                  <button onClick={() => handlePagination(page + 1)}>&#62;</button>
                  <button onClick={() => handlePagination(boardList.meta.last_page)}>&#62;&#62;</button>
                </>
              )
            }
          </div>
        )
      }

      <Link to="/board/create">
        <Button children="Write" variant="primary"/>
      </Link>
    </div>
  )
}

export default BoardList

목표

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

목표

  • 수정 페이지 만들기
  • 게시글 삭제 버튼 만들기
  • axios.patch()로 서버 데이터 수정하기
  • axios.delete()로 서버 데이터 삭제하기

 


결과 화면

 


1. state 변수 만들기

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

    1. title(타이틀)과 content(콘텐츠) state 변수를 만들고,

    Input, Textarea 컴포넌트의 onChange 속성에 state 변수를 넣어 입력할 때마다 변하는 입력값을 state 변수에 저장한다.

 

    2. axios.get()으로 서버 데이터를 받아오고,

    수정 페이지는 기존에 등록했던 title과 content의 값을 수정해야 하기 때문에 [modifyBoardData] state 변수를 만들어 초기값을 설정한다.

 

    3. 페이지가 로딩되자마자 데이터들이 보여야 하기 때문에 useEffect()로 [modifyBoardData] state 변수에 초기값을 설정한다.

const BoardModify = () => {
  // hook
  const params = useParams().id
  const navigate = useNavigate()

  // state
  let [modifyBoardData, setModifyBoardData] = useState<any>([])
  let [title, setTitle] = useState<string>(modifyBoardData.title)
  let [content, setContent] = useState<string>(modifyBoardData.content)

  useEffect(() => {
    axios.get(`http://localhost:3001/board/${params}`)
      .then((response) => {
        setModifyBoardData(response.data)
      })

      .catch(function(error) {
        navigate('/error/404')
        console.log(error)
      })
  }, [])

  useEffect(() => {
    setTitle(modifyBoardData.title)
  }, [modifyBoardData.title])

  useEffect(() => {
    setContent(modifyBoardData.content)
  }, [modifyBoardData.content])

  return (
    <div className="board-modify">
      <Title children="Write content"/>

      <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={formDelete}/>
        <Button children="Confirm" variant="primary" onClick={formSubmit}/>
        <Button children="Cancel" variant="secondary" onClick={formCancel}/>
      </div>
    </div>
  )
}

export default BoardModify

 

2. axios.patch()로 서버 데이터 수정하기

https://jae-study.tistory.com/80

Nest.js와 Psql을 사용해서 백앤드와 데이터베이스를 만들었다.

 

직접 백앤드와 데이터베이스를 만들어도 되고,

JSONPlaceholder 사이트를 통해 가짜 데이터를 받아와도 된다.

 

Postman을 통해서 PATCH로 수정한 데이터를 확인할 수 있다.

 

 

  • src/pages/board/modify/index.tsx
  1. 확인(Confirm) 버튼을 누를 때 데이터가 수정되어야 하기 때문에 formSubmit() 클릭 이벤트를 만든다.
  2. 빈 값을 전송하면 안 되기 때문에 if문을 통해 length가 0일 때 alert 창이 나오게 만든다.
  3. 수정 페이지는 서버에 데이터를 수정해야 하기 때문에 patch() 메서드를 사용한다.
  4. patch() 안에 전송할 데이터의 key와 value를 작성한다. (title, content, updated_at)
const BoardModify = () => {
  // hook
  const params = useParams().id
  const navigate = useNavigate()

  // state
  let [modifyBoardData, setModifyBoardData] = useState<any>([])
  let [title, setTitle] = useState<string>(modifyBoardData.title)
  let [content, setContent] = useState<string>(modifyBoardData.content)

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

    if(title.length === 0) {
      alert('제목을 입력해 주세요.')
    } else if(content.length === 0) {
      alert('내용을 입력해 주세요.')
    } else {
      if(window.confirm('게시글을 수정하시겠습니까?')) {
        axios.patch(`http://localhost:3001/board/${params}`, {
          title: title,
          content: content,
          updated_at: new Date()
        })
          .then(function(response) {
            alert('게시글이 수정되었습니다.')
            navigate('/board')
          })

          .catch(function(error) {
            console.log(error)
          })
      } else {
        return false
      }
    }
  }

  ...

  return (
    <div className="board-modify">
      ...
    </div>
  )
}

export default BoardModify

 

3. axios.delete()로 서버 데이터 삭제하기

  • src/pages/board/modify/index.tsx
  1. 삭제(Cancel) 버튼을 누를 때 데이터가 삭제되어야 하기 때문에 formDelete() 클릭 이벤트를 만든다.
  2. 취소(Cancel) 버튼을 누르면 게시판 목록 페이지로 갈 수 있게 formCancel() 클릭 이벤트를 만든다.
const BoardModify = () => {
  ...
  
  const formDelete= (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault()

    if(window.confirm('게시글을 삭제하시겠습니까?')) {
      axios.delete(`http://localhost:3001/board/${params}`, {
      })
        .then(function(response) {
          alert('게시글이 삭제되었습니다.')
          navigate('/board')
        })

        .catch(function(error) {
          console.log(error)
        })
    } else {
      return false
    }
  }

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

    if(window.confirm('게시글 수정을 취소하시겠습니까?')) {
      navigate('/board')
    } else {
      return false
    }
  }

  ...

  return (
    <div className="board-modify">
      ...
    </div>
  )
}

export default BoardModify

 


전체 코드

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

FIXME:: state에 데이터를 저장할 때 타입을 any로 설정했다. 수정할 예정이기 때문에 타입에 주의한다.

import { useState, useEffect } from 'react'
import { useParams } from 'react-router'
import { useNavigate } from 'react-router-dom'
import axios from 'axios'
import Button from '../../../components/form/Button'
import Input from '../../../components/form/Input'
import Label from '../../../components/form/Label'
import Textarea from '../../../components/form/Textarea'
import Title from '../../../components/text/Title'
import './index.scss'

const BoardModify = () => {
  // hook
  const params = useParams().id
  const navigate = useNavigate()

  // state
  // TODO :: any 타입 수정
  let [modifyBoardData, setModifyBoardData] = useState<any>([])
  let [title, setTitle] = useState<string>(modifyBoardData.title)
  let [content, setContent] = useState<string>(modifyBoardData.content)

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

    if(title.length === 0) {
      alert('제목을 입력해 주세요.')
    } else if(content.length === 0) {
      alert('내용을 입력해 주세요.')
    } else {
      if(window.confirm('게시글을 수정하시겠습니까?')) {
        axios.patch(`http://localhost:3001/board/${params}`, {
          title: title,
          content: content,
          updated_at: new Date()
        })
          .then(function(response) {
            alert('게시글이 수정되었습니다.')
            navigate('/board')
          })

          .catch(function(error) {
            console.log(error)
          })
      } else {
        return false
      }
    }
  }

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

    if(window.confirm('게시글을 삭제하시겠습니까?')) {
      axios.delete(`http://localhost:3001/board/${params}`, {
      })
        .then(function(response) {
          alert('게시글이 삭제되었습니다.')
          navigate('/board')
        })

        .catch(function(error) {
          console.log(error)
        })
    } else {
      return false
    }
  }

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

    if(window.confirm('게시글 수정을 취소하시겠습니까?')) {
      navigate('/board')
    } else {
      return false
    }
  }

  useEffect(() => {
    axios.get(`http://localhost:3001/board/${params}`)
      .then((response) => {
        setModifyBoardData(response.data)
      })

      .catch(function(error) {
        navigate('/error/404')
        console.log(error)
      })
  }, [])

  useEffect(() => {
    setTitle(modifyBoardData.title)
  }, [modifyBoardData.title])

  useEffect(() => {
    setContent(modifyBoardData.content)
  }, [modifyBoardData.content])

  return (
    <div className="board-modify">
      <Title children="Write content"/>

      <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={formDelete}/>
        <Button children="Confirm" variant="primary" onClick={formSubmit}/>
        <Button children="Cancel" variant="secondary" onClick={formCancel}/>
      </div>
    </div>
  )
}

export default BoardModify

 

#5
axios.patch()로 서버 데이터를 수정하고,
axios.delete()로 서버 데이터를 삭제했다.

목표

  • 생성 페이지 만들기
  • axios.post()로 서버에 데이터 전송하기

 


결과 화면

 


1. state 변수 만들기

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

title(타이틀)과 content(콘텐츠) state 변수를 만들고,

Input, Textarea 컴포넌트의 onChange 속성에 state 변수를 넣어 입력할 때마다 변하는 입력값을 state 변수에 저장한다.

const BoardCreate = () => {
  // state
  let [title, setTitle] = useState<string>('')
  let [content, setContent] = useState<string>('')

  return (
    <div className="board-create">
      <Title children="Write content"/>

      <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={formSubmit}/>
        <Button children="Cancel" variant="secondary" onClick={formCancel}/>
      </div>
    </div>
  )
}

export default BoardCreate

 

2. axios.post로 서버에 데이터 보내기

https://jae-study.tistory.com/80

Nest.js와 Psql을 사용해서 백앤드와 데이터베이스를 만들었다.

 

직접 백앤드와 데이터베이스를 만들어도 되고,

JSONPlaceholder 사이트를 통해 가짜 데이터를 받아와도 된다.

 

Postman을 통해서 POST로 전송한 데이터를 확인할 수 있다.

 

 

  • src/pages/board/create/index.tsx
  1. 확인(Confirm) 버튼을 누를 때 데이터가 전송되기 때문에 formSubmit() 클릭 이벤트를 만든다.
  2. 빈 값을 전송하면 안되기 때문에 if문을 통해 length가 0일 때 alert 창이 나오게 만든다.
  3. 생성 페이지는 서버에 데이터를 전송해야 하기 때문에 post() 메서드를 사용한다.
  4. post() 안에 전송할 데이터의 key와 value를 작성한다. (title, content)
  5. 취소(Cancel) 버튼을 누르면 게시판 목록 페이지로 갈 수 있게 formCancel() 클릭 이벤트를 만든다.
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import axios from 'axios'
import Button from '../../../components/form/Button'
import Input from '../../../components/form/Input'
import Label from '../../../components/form/Label'
import Textarea from '../../../components/form/Textarea'
import Title from '../../../components/text/Title'
import './index.scss'

const BoardCreate = () => {
  // hook
  const navigate = useNavigate()

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

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

    if(title.length === 0) {
      alert('제목을 입력해 주세요.')
    } else if(content.length === 0) {
      alert('내용을 입력해 주세요.')
    } else {
      if(window.confirm('게시글을 등록하시겠습니까?')) {
        axios.post('http://localhost:3001/board', {
          title: title,
          content: content
        })
          .then(function(response) {
            alert('게시글이 등록되었습니다.')
            navigate('/board')
          })

          .catch(function(error) {
            console.log(error)
          })
      } else {
        return false
      }
    }
  }

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

    if(window.confirm('게시글 작성을 취소하시겠습니까?')) {
      navigate('/board')
    } else {
      return false
    }
  }

  return (
    <div className="board-create">
      <Title children="Write content"/>

      <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={formSubmit}/>
        <Button children="Cancel" variant="secondary" onClick={formCancel}/>
      </div>
    </div>
  )
}

export default BoardCreate

 

#4
axios.post()로 title과 content 입력값을 서버에 전송하고,
alert 창으로 밸리데이션 역할을 대신했다.

목표

  • 상세 페이지 만들기
  • axios로 상세 페이지 서버 데이터 받기

 


결과 화면

 


1. App.tsx path 설정하기

  • src/App.tsx

상세페이지의 url이 '/board/숫자' 형식이 될 수 있도록 라우터의 path를 :id로 설정한다.

import React from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import './assets/scss/common.scss'

import BoardList from './pages/board/list'
import BoardDetail from './pages/board/detail'
import BoardCreate from './pages/board/create'
import BoardModify from './pages/board/modify'

const App = () => {
  return (
    <>
      {
        <BrowserRouter>
          <Routes>
            <Route>
              <Route path="/board" element={<BoardList/>}/>
              <Route path="/board/:id" element={<BoardDetail/>}/>
              <Route path="/board/create" element={<BoardCreate/>}/>
              <Route path="/board/modify/:id" element={<BoardModify/>}/>
            </Route>
          </Routes>
        </BrowserRouter>
      }
    </>
  )
}

export default App

 

 

2. 목록 페이지에서 상세 페이지로 이동하기

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

Link 컴포넌트의 경로를 데이터베이스의 id 값으로 설정한다.

import { Link } from 'react-router-dom'

const BoardList = () => {
  return (
    <div className="board-list">
      <table>
       ...
        <tbody>
          {
            currentPost.map((board, index: number) => {
              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. axios.get()으로 서버 데이터 받기

https://jae-study.tistory.com/80

Nest.js와 Psql을 사용해서 백앤드와 데이터베이스를 만들었다.

 

직접 백앤드와 데이터베이스를 만들어도 되고,

JSONPlaceholder 사이트를 통해 가짜 데이터를 받아와도 된다.

 

Postman을 통해서 GET으로 받아온 데이터를 확인할 수 있다.

 

 

  • src/pages/board/detail/index.tsx
  1. useEffect()를 안에 axios를 작성한다.
  2. 상세 페이지는 서버에서 데이터를 받아 화면에 보여주면 되기 때문에 get() 메서드를 사용한다.
  3. useParams() 훅을 사용해서 url의 파라미터 값에 해당하는 서버 데이터를 가지고 온다. 
  4. [setDetailBoardData] state 변수에 axios로 받아온 데이터를 저장한다.
import { useParams } from 'react-router'

const BoardDetail = () => {
  // hook
  const params = useParams().id

  // state
  const [detailBoardData, setDetailBoardData] = useState<any>([])

  useEffect(() => {
    axios.get(`http://localhost:3001/board/${params}`)
      .then((response) => {
        setDetailBoardData(response.data)
      })

      .catch(function(error) {
        console.log( error)
      })
  }, [])

  return (
    <div className="board-detail">
	  ...
    </div>
  )
}

export default BoardDetail

 

4. 서버 데이터를 화면에 출력하기

[setDetailBoardData] state 변수에 저장한 데이터를 화면에 출력한다.

const BoardDetail = () => {
  // state
  const [detailBoardData, setDetailBoardData] = useState<any>([])

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

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

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

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

      <div className="grid-2">
        <Link to={`/board/modify/${detailBoardData.id}`}>
          <Button children="Modify" variant="primary"/>
        </Link>
        <Link to="/board">
          <Button children="List" variant="secondary"/>
        </Link>
      </div>
    </div>
  )
}

export default BoardDetail

 


전체 코드

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

FIXME:: state에 데이터를 저장할 때 타입을 any로 설정했다. 수정할 예정이기 때문에 타입에 주의한다.

import { useState, useEffect } from 'react'
import { useParams } from 'react-router'
import { Link, useNavigate } from 'react-router-dom'
import axios from 'axios'
import dayjs from 'dayjs'
import Button from '../../../components/form/Button'
import Title from '../../../components/text/Title'
import './index.scss'

const BoardDetail = () => {
  // hook
  const params = useParams().id
  const navigate = useNavigate()

  // state
  const [detailBoardData, setDetailBoardData] = useState<any>([])

  useEffect(() => {
    axios.get(`http://localhost:3001/board/${params}`)
      .then((response) => {
        setDetailBoardData(response.data)
      })

      .catch(function(error) {
        console.log( error)
      })
  }, [])

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

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

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

      <div className="grid-2">
        <Link to={`/board/modify/${detailBoardData.id}`}>
          <Button children="Modify" variant="primary"/>
        </Link>
        <Link to="/board">
          <Button children="List" variant="secondary"/>
        </Link>
      </div>
    </div>
  )
}

export default BoardDetail

 

#3
axios.get()과 useParams()을 통해
id별로 각각의 상세 페이지를 보여줄 수 있도록 만들었다.

목표

  • react-js-pagination 패키지를 사용해서 페이지네이션 컴포넌트 만들기
  • 페이지네이션 기능 구현

 


결과 화면

 


1. react-js-pagination 패키지 설치하기

https://www.npmjs.com/package/react-js-pagination

react-js-pagination 패키지에 대한 자세한 설명은 npm 사이트를 참고한다.

 

타입스크립트로 설치된 리액트이기 때문에 타입스크립트 버전도 같이 설치한다.

yarn add react-js-pagination @types/react-js-pagination
또는
npm install react-js-pagination @types/react-js-pagination

 

2. Pagination 컴포넌트 css 또는 scss 작성하기

react-js-pagination 패키지의 경우, css가 없기 때문에 직접 css 또는 scss를 작성한다.

 

  • src/assets/scss/components/pagination.scss

css/scss를 작성하고, 공통 css/scss에 import 하거나 페이지네이션을 사용한 페이지에 import 한다.

해당 scss는 예시일 뿐, 원하는 디자인으로 만든다.

.pagination {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 5px;
  margin: 50px auto;

  li {
    width: 30px;
    height: 30px;
    border-radius: 50%;
    background-color: $bg-light;
    transition: all 0.3s;

    &:hover,
    &:active ,
    &.active{
      background-color: $bg-point;
      color: $white;
    }

    &:nth-child(1),
    &:nth-child(2),
    &:nth-last-child(1),
    &:nth-last-child(2) {
      a {
        align-items: baseline;
        font-size: 20px;
      }
    }

    a {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 100%;
      height: 100%;
      font-size: 12px;
    }
  }
}

 

3. react-js-pagination 패키지의 Pagination 컴포넌트 사용하기

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

axios로 받은 전체 게시글 데이터를 slice() 해서 페이지네이션 별로 화면에 보여준다.

 

  1. Pagination 컴포넌트를 import 한다.
  2. [currentPost, setCurrentPost], [page, setPage] state 변수를 만든다.
  3. postPerPage, indexOfLastPost, indexOfFirstPost 변수를 만든다.
  4. handlePageChange() 클릭 이벤트를 만든다. 페이지네이션을 누를 때마다 몇 번째 페이지인지 알 수 있다.
  5. 최근 게시물부터 역순으로 출력하고 싶기 때문에 axios 데이터를 reverse() 한다.
  6. 전체 게시글 또는 페이지네이션이 바뀔 때마다 동작해야 하기 때문에 useEffect() 안에 [setCurrentPost] state 변수를 작성한다.
  7. axios 데이터를 첫 번째 게시글 인덱스 번호와 마지막 게시글 인덱스 번호로 slice() 해서 5개씩 노출시킨다.
import Pagination from 'react-js-pagination'

const BoardList = () => {
  const [boardList, setBoardList] = useState<BoardType[]>([]) // axios에서 받아온 전체 게시글 데이터
  const [currentPost, setCurrentPost] = useState<BoardType[]>(boardList) // 페이지네이션을 통해 보여줄 게시글
  const [page, setPage] = useState<number>(1) // 현재 페이지 번호

  const postPerPage: number = 5 // 페이지 당 게시글 개수
  const indexOfLastPost: number = page * postPerPage
  const indexOfFirstPost: number = indexOfLastPost - postPerPage

  const handlePageChange = (page: number) => {
    setPage(page)
  }

  useEffect(() => {
    axios.get('http://localhost:3001/board')
      .then((response) => {
        setBoardList([...response.data].reverse())
      })

      .catch(function(error) {
        console.log(error)
      })
  }, [])

  useEffect(() => {
    setCurrentPost(boardList.slice(indexOfFirstPost, indexOfLastPost))
  }, [boardList, page])

  return (
    <div className="board-list">
      <table>
        ...
      </table>

      <Pagination
        activePage={page}
        itemsCountPerPage={postPerPage}
        totalItemsCount={boardList.length}
        pageRangeDisplayed={5}
        prevPageText={"‹"}
        nextPageText={"›"}
        onChange={handlePageChange}/>
    </div>
  )
}

export default BoardList

 


전체 코드

  • src/pages/board/list/index.tsx
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import axios from 'axios'
import dayjs from 'dayjs'
import Pagination from 'react-js-pagination'
import { BoardType } from '../../../interface/BoardType'
import Button from '../../../components/form/Button'
import Title from '../../../components/text/Title'
import './index.scss'

const BoardList = () => {
  // state
  const [boardList, setBoardList] = useState<BoardType[]>([]) // axios에서 받아온 게시글 데이터
  const [currentPost, setCurrentPost] = useState<BoardType[]>(boardList) // 게시판 목록에 보여줄 게시글
  const [page, setPage] = useState<number>(1) // 현재 페이지 번호

  const postPerPage = 5 // 페이지 당 게시글 개수
  const indexOfLastPost = page * postPerPage
  const indexOfFirstPost = indexOfLastPost - postPerPage

  const boardLength = boardList.length

  const handlePageChange = (page: number) => {
    setPage(page)
  }

  useEffect(() => {
    axios.get('http://localhost:3001/board')
      .then((response) => {
        setBoardList([...response.data].reverse())
      })

      .catch(function(error) {
        console.log(error)
      })
  }, [])

  useEffect(() => {
    setCurrentPost(boardList.slice(indexOfFirstPost, indexOfLastPost))
  }, [boardList, page])

  return (
    <div className="board-list">
      <Title children="Board list"/>

      <h4>Total post : {boardLength}</h4>

      <table>
        <colgroup>
          <col width="15%"/>
          <col width="65%"/>
          <col width="20%"/>
        </colgroup>

        <thead>
          <tr>
            <th>No</th>
            <th>Title</th>
            <th>Date</th>
          </tr>
        </thead>

        <tbody>
          {
            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>

      <Pagination
        activePage={page}
        itemsCountPerPage={postPerPage}
        totalItemsCount={boardList.length}
        pageRangeDisplayed={5}
        prevPageText={"‹"}
        nextPageText={"›"}
        onChange={handlePageChange}/>

      <Link to="/board/create">
        <Button children="Write" variant="primary"/>
      </Link>
    </div>
  )
}

export default BoardList

 

#2.2
react-js-paginatnio 패키지를 설치하고,
scss를 작성해 디자인을 커스텀하고,
axios 데이터를 slice 해서
페이지네이션 기능이 동작하게 만들었다.

목표

  • 목록 페이지 만들기
  • axios.get()으로 목록 페이지 서버 데이터 받기
  • dayjs 패키지를 이용해 날짜 포맷하기

 


결과 화면

 


1. axios 패키기 설치하기

axios 패키기를 설치한다.

이미 설치되어 있다면 설치하지 않아도 된다.

yarn add axios
또는
npm install axios

 

2. axios.get()으로 서버 데이터 받기

https://jae-study.tistory.com/80

Nest.js와 Psql을 사용해서 백앤드와 데이터베이스를 만들었다.

 

직접 백앤드와 데이터베이스를 만들어도 되고,

JSONPlaceholder 사이트를 통해 가짜 데이터를 받아와도 된다.

 

Postman을 통해서 GET으로 받아온 데이터를 확인할 수 있다.

 

 

  • src/pages/board/list/index.tsx
  1. useEffect()를 안에 axios를 작성한다.
  2. 목록 페이지는 서버에서 데이터를 받아 화면에 보여주면 되기 때문에 get() 메서드를 사용한다.
  3. [setBoardList] state 변수에 axios로 받아온 데이터를 저장한다.
const BoardList = () => {
  const [boardList, setBoardList] = useState<BoardType[]>([])

  useEffect(() => {
    axios.get('http://localhost:3001/board')
      .then((response) => {
        setBoardList(response.data)
      })

      .catch(function(error) {
        console.log(error)
      })
  }, [])

  return (
    <div className="board-list">
      <table>
	...
      </table>
    </div>
  )
}

export default BoardList

 

3. 서버 데이터를 화면에 출력하기

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

[setBoardList] state 변수에 저장한 데이터를 map() 메서드를 사용해 화면에 출력한다.

const BoardList = () => {
  const [boardList, setBoardList] = useState<BoardType[]>([])

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

  return (
    <div className="board-list">
        ...
        <tbody>
          {
            boardList.map((board, index) => {
              return (
                <tr key={index}>
                  <td>{index + 1}</td>
                  <td className="title">
                    <Link to={`/board/${board.id}`}>{board.title}</Link>
                  </td>
                  <td>{board.created_at}</td>
                </tr>
              )
            })
          }
        </tbody>
      </table>
    </div>
  )
}

 

4. dayjs 패키지를 사용해 날짜 포맷하기

dayjs 패키지를 설치한다.

yarn add dayjs
또는 
npm install dayjs

 

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

dayjs().format()에 원하는 날짜 형식을 작성한다.

const BoardList = () => {
  const [boardList, setBoardList] = useState<BoardType[]>([])

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

  return (
    <div className="board-list">
        ...
        <tbody>
          {
            boardList.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>
  )
}

 


전체 코드

  • src/pages/board/list/index.tsx
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import axios from 'axios'
import dayjs from 'dayjs'
import { BoardType } from '../../../interface/BoardType'
import Title from '../../../components/text/Title'
import './index.scss'

const BoardList = () => {
  const [boardList, setBoardList] = useState<BoardType[]>([])

  const boardLength = boardList.length

  useEffect(() => {
    axios.get('http://localhost:3001/board')
      .then((response) => {
        setBoardList(response.data)
      })

      .catch(function(error) {
        console.log(error)
      })
  }, [])

  return (
    <div className="board-list">
      <Title children="Board list"/>

      <h4>Total post : {boardLength}</h4>

      <table>
        <colgroup>
          <col width="15%"/>
          <col width="65%"/>
          <col width="20%"/>
        </colgroup>

        <thead>
          <tr>
            <th>No</th>
            <th>Title</th>
            <th>Date</th>
          </tr>
        </thead>

        <tbody>
          {
            boardList.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

 

#2.1
axios.get()을 통해서 서버 데이터를 받아오고,
map() 메서드를 통해 화면에 출력한 뒤,
dayjs 패키지를 사용해서 날짜를 포맷했다.

목표

  • 게시판 목록 페이지 만들기
  • 게시판 상세 페이지 만들기
  • 게시판 작성 페이지 만들기
  • 게시판 수정 및 삭제 페이지 만들기

Typescript로 React를 설치하고, 게시판에 필요한 페이지들을 만든다.

 


결과 화면

 


1. 타입스크립트로 리액트 프로젝트 설치하기

https://jae-study.tistory.com/70

리액트 설치에 대한 자세한 설명은 위의 링크를 참고한다.

 

터미널에서 아래 명령어를 실행해 타입스크립트로 리액트 프로젝트를 설치한다.

yarn create react-app 폴더명 --template typescript
또는
npx create-react-app 폴더명 --template typescript

 

 

2. 게시판 폴더 구조

  • 목록 : src/pages/board/list/index.tsx
  • 상세 : src/pages/board/detail/index.tsx
  • 생성 : src/pages/board/create/index.tsx
  • 수정 : src/pages/board/modify/index.tsx

게시판 목록, 상세, 생성, 수정, 총 4가지 페이지를 만든다.

 

 

3. 마크업, css/scss 적용하기

https://jae-study.tistory.com/72

리액트에 scss를 적용하고 싶으면 위의 링크를 참고한다.

 

페이지 별로 마크업을 하고,

css 또는 scss 파일을 작성한 뒤, 각 index.tsx 파일에 import 해준다.

 

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

해당 마크업은 예시일 뿐, 원하는 디자인으로 목록, 상세, 생성, 수정 페이지를 만든다.

import { Link } from 'react-router-dom'
import Button from '../../../components/form/Button'
import Title from '../../../components/text/Title'
import './index.scss'

const BoardList = () => {
  return (
    <div className="board-list">
      <Title children="Board list"/>

      <h4>Total post : 10개</h4>

      <table>
        <colgroup>
          <col width="15%"/>
          <col width="65%"/>
          <col width="20%"/>
        </colgroup>

        <thead>
          <tr>
            <th>No</th>
            <th>Title</th>
            <th>Date</th>
          </tr>
        </thead>

        <tbody>
          <tr>
            <td>1</td>
            <td>
              <Link to={`/board/1`}>공지사항 입니다.</Link>
            </td>
            <td>2023.06.30</td>
          </tr>
        </tbody>
      </table>

      <Link to="/board/create">
        <Button children="Write" variant="primary"/>
      </Link>
    </div>
  )
}

export default BoardList

 

  • src/pages/board/list/index.scss
@import '../../../assets/scss/base/variable';

.board-list {
  h4 {
    margin-bottom: 20px;
    font-weight: 700;
  }

  table {
    background-color: $bg-light;
    border-radius: 5px;

    thead {
      th {
        padding: 20px 0;
        font-size: 20px;
        font-weight: 700;
      }
    }

    tbody {
      border-top: 1px solid $bg-point;

      td {
        padding: 25px 0;
        text-align: center;
        vertical-align: middle;

        &.title {
          text-align: left;

          a {
            padding-bottom: 2px;

            &:hover {
              border-bottom: 1px solid $bg-point;
            }
          }
        }
      }
    }
  }
}

 

4. 라우터 연결하기

https://jae-study.tistory.com/73

react-router-dom에 대한 자세한 설명은 위의 링크를 참고한다.

 

react-router-dom 패키지를 설치한다.

yarn add react-router-dom
또는
npm install react-router-dom

 

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

const root = ReactDOM.createRoot (
  document.getElementById('root') as HTMLElement
)

root.render(
  <React.StrictMode>
    <App/>
  </React.StrictMode>
)

 

  • src/App.tsx

BrowserRouter, Routes, Route 패키지를 import 하고,

<Route> 컴포넌트의 path를 설정한다.

게시판 상세와 수정 페이지는 게시글 별로 페이지가 있기 때문에 :id 값을 받아야 한다.

import React from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import './assets/scss/common.scss'

import BoardList from './pages/board/list'
import BoardDetail from './pages/board/detail'
import BoardCreate from './pages/board/create'
import BoardModify from './pages/board/modify'

const App = () => {
  return (
    <>
      {
        <BrowserRouter>
          <Routes>
            <Route path="/board" element={<BoardList/>}/>
            <Route path="/board/:id" element={<BoardDetail/>}/>
            <Route path="/board/create" element={<BoardCreate/>}/>
            <Route path="/board/modify/:id" element={<BoardModify/>}/>
          </Routes>
        </BrowserRouter>
      }
    </>
  )
}

export default App

 

#1
타입스크립트로 리액트를 설치하고,
게시판 목록, 상세, 생성, 수정 페이지를 만들고,
라우터 연결까지 완료했다.

+ Recent posts