왕초보를 위한 React Native 101

https://nomadcoders.co/react-native-for-beginners

 

노마드코더 강의를 참고하고 있습니다.

 


리액트 네이티브란?

리액트 네이티브(React Native)는 페이스북이 개발한 오픈 소스 모바일 애플리케이션 프레임워크로서, React와 JavaScript를 사용하여 Android 및 IOS 모바일 애플리케이션을 개발할 수 있다.

 

특징

  • 리액트를 기반으로 하고 있어 컴포넌트를 사용해 재사용성이 높은 UI 구성요소를 만들 수 있다.
  • 앱이 실행 중일 때 코드를 변경하면 핫 리로딩을 통해 변경 사항을 즉시 반영할 수 있다.
  • 네이티브 모듈을 통해 카메라, 위치 정보, 파일 시스템 등 네이티브 기능에 직접 액세스 할 수 있다.
  • Expo 툴킷을 사용해 별도의 네이티브 코드를 작성하지 않고, 앱을 빠르게 개발할 수 있다.

 

장점

  • 리액트 네이티브는 React와 JavaScript를 사용하기 때문에 동일한 코드 베이스로 Android와 IOS 모바일 애플리케이션 개발이 가능하다.
  • 리액트 네이티브는 활발한 커뮤니티와 다양한 패키지를 가지고 있어 문제 해결이나 새로운 기능 추가에 대해 도움을 받을 수 있다.
  • 네이티브 모듈을 활용해 개발 생산성이 향상된다.
  • 핫 리로딩 기능이 활용해 개발 주기가 빠르다.

 

단점

  • 네이티브 앱(특정 플랫폼 또는 운영 체제에 최적화되어 개발된 모바일 애플리케이션)에 비해 성능의 제약이 있을 수 있다.
  • 특정 플랫폼에 특화된 기능이 필요한 경우, 네이티브 모듈의 사용해야 해서 추가적인 작업이 발생할 수 있다.

 


Expo란

Expo는 리액트 네이티브 앱을 개발하고 배포하기 위한 도구와 서비스를 제공하는 오픈 소스 프레임워크이다.

Expo를 사용하면 네이티브 개발 환경(Java, Android studio 등)을 설정하는 번거로움 없이 빠르게 리액트 네이티브 애플리케이션을 개발할 수 있다.

 

Expo로 리액트 네이티브 설치하기

https://expo.dev/

 

Expo

Expo is an open-source platform for making universal native apps for Android, iOS, and the web with JavaScript and React.

expo.dev

노마드 강의는 2021년 버전이기 때문에 Expo 공식 문서를 참고해 리액트 네이티브를 설치했다.

 

 

node 버전 18 이상

1. expo-app 설치하기

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

npx create-expo-app 폴더명

 

2. 종속성 설치하기

설치한 폴더로 이동 후, 아래 패키지를 설치한다.

npx expo install react-dom react-native-web @expo/webpack-config

 

3. 웹에서 실행하기

터미널에 qr 코드만 나오고 웹이 실행되지 않는다면 w 옵션(open web)을 누른다.

npx expo start

 

4. 웹 결과 화면

 

 

5. 모바일(핸드폰)에서 확인하기

안드로이드를 사용하면 플레이 스토어에서 Expo 앱 설치하기

IOS를 사용하면 앱 스토어에서 Expo Go 앱 설치하기

 

앱 설치가 완료된 후, 회원가입과 로그인을 한다.

 

핸드폰으로 터미널에 나온 qr 코드를 스캔하거나

터미널에서 npx expo login 명령어를 입력해 expo에 로그인을 한다.

 

Expo 앱에 내 프로젝트가 연결된 것을 확인할 수 있다.

※ 주의, 컴퓨터와 핸드폰 와이파이가 같은 인터넷을 사용해야만 핸드폰에서 확인이 가능하다. ※

 

6. 브라우저에서 코딩 및 앱 화면 확인하기

https://snack.expo.dev/

만약 브라우저만 사용해서 코딩을 해야 하는 상황이라면 위의 홈페이지에서 작업을 하면 된다.

어떠한 설치 없이 중앙 편집기에서 코드를 작성하면 우측에서 IOS, Android, Web의 결과 화면을 확인할 수 있다.

또한 회원가입 및 로그인을 한 후에 저장을 하면 나만의 URL이 생성된다.

결과 화면

 


수정사항

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

목표

  • Formik 컴포넌트 사용하기
  • Yup 패키지로 유효성 검사(validation) 처리하기

 


결과 화면

 


1. yup 설치하기

yup은 값 구문 분석 및 유효성 검사를 위한 스키마 빌더이다. (타입스크립트도 지원)

 

https://www.npmjs.com/package/yup?activeTab=readme 

yup에 대한 자세한 설명은 위의 링크를 참고한다.

yarn add yup
또는 
npm install yup

 

2. yup 스키마 작성하기

min() : 최소 글자수

max() : 최대 글자수

matches() : 정규 표현식

required() : 필수값

 

input 유효성 검사에서 가장 많이 사용하는 4가지 메서드이고,

email, url 등 다양한 메서드들을 사용하고 싶다면 yup 공식 문서를 참고한다.

import * as Yup from 'yup'

const joinSchema = Yup.object().shape({
  userId: Yup.string()
    .min(4, '4자 이상의 아이디를 입력해 주세요.')
    .max(10, '10자 이하의 아이디를 입력해 주세요.')
    .matches(/^(?=.*[a-z])(?=.*[0-9]).{4,10}$/, '영문 소문자와 숫자를 조합해서 아이디를 입력해 주세요.')
    .required('아이디를 입력해 주세요.'),
  password: Yup.string()
    .min(4, '4자 이상의 비밀번호를 입력해 주세요.')
    .max(10, '10자 이하의 비밀번호를 입력해 주세요.')
    .matches(/^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^*+=-]).{4,10}$/, '영문, 숫자, 특수문자를 조합해서 비밀번호를 입력해 주세요.')
    .required('비밀번호를 입력해 주세요.')
})

 

3. Formik 컴포넌트 설치하기

Formik은 Reac와 React Native 위한 form 오픈 소스 라이브러리이다.

입력 유효성 검사, 서식 지정, 배열 및 오류 처리 등 form 코드를 간단하게 작성할 수 있다.

 

https://formik.org/docs/overview

Formik에 대한 자세한 설명은 위의 링크를 참고한다.

yarn add formik
또는
npm install formik

 

4. Form 컴포넌트 적용하기

Formik, Form, Field, ErrorMessage 컴포넌트는 Formik 라이브러리에서 제공하는 컴포넌트이다.

 

initialValues : 초기값

validationSchema : yup 스키마 변수

onSubmit : form이 제출됐을 때 동작하는 함수

 

Field와 ErrorMessage 컴포넌트에 intivalValues에 해당하는 name을 작성한다.

button의 type을 submit으로 한다.

 

폼을 제출하면 알아서 validation 처리가 이루어진다.

import * as Yup from 'yup'
import { Formik, Form, Field, ErrorMessage } from 'formik'
...

const joinSchema = Yup.object().shape({
  userId: Yup.string()
    .min(4, '4자 이상의 아이디를 입력해 주세요.')
    .max(10, '10자 이하의 아이디를 입력해 주세요.')
    .matches(/^(?=.*[a-z])(?=.*[0-9]).{4,10}$/, '영문 소문자와 숫자를 조합해서 아이디를 입력해 주세요.')
    .required('아이디를 입력해 주세요.'),
  password: Yup.string()
    .min(4, '4자 이상의 비밀번호를 입력해 주세요.')
    .max(10, '10자 이하의 비밀번호를 입력해 주세요.')
    .matches(/^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^*+=-]).{4,10}$/, '영문, 숫자, 특수문자를 조합해서 비밀번호를 입력해 주세요.')
    .required('비밀번호를 입력해 주세요.')
})

const Join = () => {
  ...

  return (
    <div className="join">
      <Formik
        initialValues={{
          userId: '',
          password: ''
        }}
        validationSchema={ joinSchema }
        onSubmit={ values => { dispatch(postJoin(values)) }}>
        <Form>
          <div>
            <Label htmlFor="userId">Id</Label>
            <Field name="userId" placeholder="아이디를 입력해 주세요."/>
            <ErrorMessage name="userId" component="span" className="error" />
          </div>

          <div>
            <Label htmlFor="password">Password</Label>
            <Field type="password" name="password" placeholder="비밀번호를 입력해 주세요."/>
            <ErrorMessage name="password" component="span" className="error" />
          </div>
          
          <Button type="submit" children="Confirm" variant="primary"/>
        </Form>
      </Formik>
    </div>
  )
}

export default Join

목표

  • 생성, 수정, 삭제 페이지에 직접 작성한 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 해서
페이지네이션 기능이 동작하게 만들었다.

+ Recent posts