v-if

v-if 디렉티비는 조건부로 블록을 렌더링 하고, 블록은 디렉티브 표현식이 ture 일 때만 렌더링 된다.

 

v-else

v-if에 대한 else 블록을 렌더링 하고, 블록은 디렉티브 표현식이 false 일 때만 렌더링 된다.

 

결과 화면

 

  • pages/index.vue

실제 프로젝트를 진행할 때에는 데이터(게시판, 공지사항 등)는 API로 받게 된다. 

데이터가 아무것도 없을 때에도 레이아웃을 보여주어야 하기 때문에 v-if와 v-else를 사용해 렌더링을 한다.

 

v-if 조건문에 맞춰 dataList 배열의 길이가 0이면 <h2> 태그를 렌더링 하고,

dataList 배열의 길이가 0이 아니라면 <ul> 태그를 렌더링 한다.

<template>
  <TheLayout>
    <h2 v-if="dataList.length === 0">데이터가 없습니다.</h2>

    <ul v-else>
      <li
        v-for="(list, key) in dataList"
        :key="`list-${key}`">
        <h2>{{ list.title }}</h2>
        <p>{{ list.content }}</p>
        <span>{{ list.date }}</span>
      </li>
    </ul>
  </TheLayout>
</template>

<script>
import TheLayout from "@/components/layout/TheLayout"

export default {
  name: 'Main',

  components: {
    TheLayout
  },

  data() {
    return {
      dataList: []
    }
  }
}
</script>

v-for

게시판의 게시글 리스트나 반복적인 내용을 보여줘야 할 때 v-for 디렉티브를 사용한다.

v-for="item in items":key 형태이다.

뷰에서 사용하는 for문이라고 생각하면 된다.

 

결과 화면

 

v-for 사용하기

  • pages/index.vue
  1. <script> 태그 안 data() 영역에 배열을 만든다. (dataList)
  2. <li> 태그가 반복되어야 하기 때문에 <li> 태그에 v-for와 :key를 작성한다.
  3. 중괄호 {{ ~ }} 안에 배열의 속성값을 작성한다.
<template>
  <TheLayout>
    <ul>
      <li
        v-for="(list, key) in dataList"
        :key="`list-${key}`">
        <h2>{{ list.title }}</h2>
        <p>{{ list.content }}</p>
        <span>{{ list.date }}</span>
      </li>
    </ul>
  </TheLayout>
</template>

<script>
import TheLayout from "@/components/layout/TheLayout"

export default {
  name: 'Main',

  components: {
    TheLayout
  },

  data() {
    return {
      dataList: [
        {
          title: 'Nuxt.js',
          content: 'Nuxt.js란, Vue.js 애플리케이션을 구축할 수 있는 오픈 소스 프레임워크이다.',
          date: '2023-10-01'
        },
        {
          title: 'Slot',
          content: '컴포넌트를 랜더링 할 때 html로 작성한 코드가 컴포넌트의 slot 부분으로 교체된다.',
          date: '2023-10-02'
        },
        {
          title: '뷰 SASS/SCSS 적용하기',
          content: 'sass/scss를 적용하고 싶은 .vue 파일에서 script 태그 아래에 style 태그를 작성한다.',
          date: '2023-10-03'
        }
      ]
    }
  }
}
</script>

props

props는 부모 컴포넌트의 데이터를 자식 컴포넌트로 전달한다.

부모 > 자식

 

emit

자식 컴포넌트에서 $emit 이벤트를 이용해 부모 컴포넌트에서 만들어 놓은 이벤트를 호출한다.

자식 > 부모

 

BaseButton 만들기

  1. BaseButton.vue 파일을 만들어 버튼을 공통으로 사용한다.
  2. class 속성을 props로 전달받아 css를 적용한다.
  3. $emit를 사용해 부모 컴포넌트에 작성한 이벤트를 호출한다.

 

결과 화면

 

1. BaseButton 컴포넌트 만들기

  • components/base/BaseButton.vue (자식 컴포넌트)
  1. :class="[color]" color 라는 속성을 만들어 부모 컴포넌트에서 색상 값을 전달받는다. (배열 형태로 만들면 여러 가지 값을 전달받을 수 있다.)
  2. <script> 태그 안 props 부분에 color에 대한 타입을 정의한다. (타입을 정의하지 않으면 잘못된 값을 전달받았을 때 콘솔 오류가 발생한다.)
  3. 전달받은 color에 대한 css를 작성한다. (.red, .blue)
  4. @click="$emit('click')" $emit을 이용해 클릭 이벤트를 호출한다. (클릭 이벤트는 부모 컴포넌트에 만든다.)
<template>
  <button
    type="button"
    :class="[color]"
    @click="$emit('click')">
    <slot/>
  </button>
</template>

<script>
export default {
  name: 'BaseButton',

  props: {
    color: {
      type: String,
      required: false
    }
  }
}
</script>

<style lang="scss" scoped>
button {
  width: 150px;
  height: 40px;

  &.red {
    background-color: red;
  }

  &.blue {
    background-color: blue;
  }
}
</style>

 

2. BaseButton 컴포넌트 사용하기

  • pages/index.vue (부모 컴포넌트)
  1. <BaseButton> 컴포넌트를 import 한다.
  2. <BaseButton> 컴포넌트에 color 속성을 작성한다. (color="red", color="blue")
  3. @click 메서드를 통해 클릭 이벤트를 호출한다.
  4. <script> 태그 안 methods 부분에 클릭 이벤트를 작성한다. (redAlert(), blueAlert())
<template>
  <TheLayout>
    <BaseButton
      color="red"
      @click="redAlert">
      빨간색 버튼
    </BaseButton>

    <BaseButton
      color="blue"
      @click="blueAlert">
      파란색 버튼
    </BaseButton>
  </TheLayout>
</template>

<script>
import TheLayout from "@/components/layout/TheLayout"
import BaseButton from "@/components/base/BaseButton"

export default {
  name: 'Main',

  components: {
    TheLayout,
    BaseButton
  },

  methods: {
    redAlert() {
      alert('빨간색 버튼')
    },

    blueAlert() {
      alert('파란색 버튼')
    }
  }
}
</script>

1. sass, sass-loader 설치하기

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

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

yarn add sass sass-loader

또는

npm install sass sass-loader

 

 

2. style 작성하기

  • pages/index.vue

sass/scss를 적용하고 싶은 .vue 파일에서 <script> 태그 아래에 <style> 태그를 작성한다.

<template>
  <TheLayout>
    <h1>메인 페이지</h1>
  </TheLayout>
</template>

<script>
import TheLayout from "@/components/layout/TheLayout"

export default {
  name: 'Main',

  components: {
    TheLayout
  }
}
</script>

<style lang="scss" scoped>
  h1 {
    color: blue
  }
</style>

 

3. scss 적용이 안됐을 경우

버전 차이로 인해 sass-loader가 작동을 안 할 때가 있다.

sass-loader를 삭제 후, 버전 10을 설치한다.

// 삭제하기
yarn remove sass-loader

// 버전 10 설치하기
yarn add sass-loader@10

 

4. reset.scss 등 전역 scss 적용하기

  • static/css/common.scss

reset, font, layout 등 공통으로 적용하고 싶은 scss 파일이 있다면 매번 스타일을 작성하거나 import 시키는 것보다 전역에서 적용하는 것이 효율적이다.

위의 경로에 common.scss 파일을 만들고, 공통으로 적용하고 싶은 scss 파일을 import 시킨다.

// base
@import './base/reset';
@import './base/font';

// common
@import './common/layout';
@import './common/transition';

...

 

5. nuxt.config.js

nuxt.config.js 파일을 열고, css 부분에 common.scss 파일을 작성한다.

common.scss에 import 시켜둔 모든 scss 파일이 전역에서 적용된 것을 확인할 수 있다.

export default {
  ...
  
  // Global CSS: https://go.nuxtjs.dev/config-css
  css: [
    '@/static/css/common.scss'
  ],
  
  ...
}

 

6. 결과 화면

slot, 슬롯이란

컴포넌트를 랜더링 할 때 html로 작성한 코드가 컴포넌트의 <slot></slot> 부분으로 교체된다.

 

1. 레이아웃 컴포넌트 만들기

  • components/layout/TheLayout.vue

<header>, <footer>는 공통으로 사용하고, <main> 영역만 변경하고 싶어 <slot>을 사용했다.

<template>
  <div>
    <header>헤더</header>

    <main>
      <slot/>
    </main>

    <footer>푸터</footer>
  </div>
</template>

<script>
export default {
  name: 'TheLayout'
}
</script>

 

2. 레이아웃 컴포넌트 import 시키기

  • pages/index.vue

<h1> 이라고 작성한 부분이 <TheLayout> 컴포넌트의 <slot> 영역으로 교체된다.

<template>
  <TheLayout>
    <h1>메인 페이지</h1>
  </TheLayout>
</template>

<script>
import TheLayout from "@/components/layout/TheLayout"

export default {
  name: 'Main',

  components: {
    TheLayout
  }
}
</script>

 

3. 결과 화면

Nuxt.js란

Nuxt.js란, Vue.js 애플리케이션을 구축할 수 있는 오픈 소스 프레임워크이다.

Nuxt.js 공식 문서: https://nuxt.com/

  • 코드 분할
  • SSR, SPA 등 렌더링 결정 가능
  • 서버 측 렌더링
  • 최적화된 이미지
  • SEO

 


1. npm init nuxt-app

터미널에서 아래 명령어를 실행하고, 나오는 옵션들을 선택한다.

프로젝트 이름, Javascript/Typescript 언어 선택 등을 설정할 수 있다.

잘 모르겠으면 전부 앤터를 누른다.

npm init nuxt-app 프로젝트 이름

 

 

2. 로컬호스트 연결 확인하기

설치한 프로젝트로 이동 또는 프로젝트를 열고, 터미널에서 아래 명령어를 실행한다.

yarn dev

또는

yarn build
yarn start

 

 

3. 결과 화면

로컬 호스트 접속 후, 아래 화면이 나온다면 로컬호스트 연결에 성공한 것이다.

 

 

4. 폴더 구조

정규 표현식 또는 정규식은 문자열에서 특정 문자 조합을 찾기 위한 패턴이다.

 

https://regexr.com/

위의 링크에서 정규 표현식을 테스트할 수 있다.

 

  • *

문자 또는 숫자가 0개 이상이다.

 

  • +

문자 또는 숫자가 1개 이상이다.

 

  • ^

문자열 또는 줄의 시작을 지정한다.

^[0]은 0으로 시작하는 문자열과 일치한다.

 

  • $

문자열 또는 줄의 끝을 지정한다.

[0]$은 0으로 끝나는 문자열과 일치한다.

 

  • [...]

대괄호는 대괄호 안의 문자와 일치한다.

예를 들어, [a-z]는 a부터 z 사이의 소문자와 일치한다.

 

  • {...}

중괄호는 반복을 나타낸다.

[a]{2}는 2개의 a가 있는 문자와 일치하고, [0-9]{2, 5}는 2~5개의 숫자를 가진 문자와 일치한다.

 


영어 소문자만 가능

ex) abc..

/^[a-z]+$/g

 

영어 대문자만 가능

ex) ABC..

/^[A-Z]+$/g

 

영문만 가능

ex) abcABC..

/^[a-zA-Z]+$/g

 

한글만 가능

ex) 가나다..

/^[가-힣]+$/g

 

숫자만 가능

ex) 123..

/^[0-9]+$/g

 

주민번호

ex) 230101-1010101..

/^\d{6}-[1|2|3|4]{1}\d{6}$/g
또는
/^[0-9]{6}-[1|2|3|4]{1}[0-9]{6}$/g

 

전화번호

ex) 021234567, 01012345678..

/^[0-9]{9,11}/g

 

- 전화번호

ex) 02-123-1234, 010-1234-5678..

/^\d{2,3}-\d{3,4}-\d{4}$/g
또는
/^[0-9]{2,3}-[0-9]{3,4}-[0-9]{4}$/g

 

이메일

ex) test123@test.com..

/^[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[a-zA-Z]{2,3}$/g
또는
/^[a-zA-Z0-9]+@[a-zA-Z0-9]+\.(com|net|kr)$/g

 

특수문자

/^[\{\}\[\]\/?.,;:|\)*~`!^\-_+<>@\#$%&\\\=\(\'\"]+$/g

목표

  • 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를 통해
목록 페이지와 상세페이지를 만들었다.

+ Recent posts