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

 

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

목표

  • 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을 사용해서 동작하게 만들 것이다.

10-1 제네릭 타입 이해하기

제네릭 타입이란, 인터페이스나 클래스, 함수, 타입 별칭 등에 사용할 수 있는 기능으로, 해당 심벌의 타입을 미리 지정하지 않고 한 가지 타입보다 여러 가지 타입에서 동작하는 컴포넌트를 생성할 때 사용한다.

string, number 등으로 특정하지 않고, T로 지정해 제네릭 타입을 만든다.

// 제네릭 인터페이스
interface IValuable<T> {
    value: T
}

// 제네릭 함수
function identity<T>(arg: T): T {
  return arg
}

// 제네릭 함수 - 화살표 함수
const fn1 = <T>(a: T): void => {}
const fn2 = <T, Q>(a: T, b: Q): void => {}

// 제네릭 클래스
class Valuable<T> {
  constructor(public value: T) {}
}

// 제네릭 타입 별칭
type IVable<T> = {
  value: T
}

// 제네릭 타입 별칭
type Type1Func<T> = (T) => void
type Type2Func<T, Q> = (T, Q) => void
type Type3Func<T, Q, R> = (T, Q) => R

 

  • 제네릭 동작 방식
function getText<T>(text: T): T {
  return text
}

getText<string>('hi')

function getText<string>(text: string): string {
  return text
}

 

제네릭 사용하기

  1. 제네릭 인터페이스를 정의한다.
  2. 제네릭 클래스는 자신이 가진 타입 변수 T를 인터페이스 쪽 제네릭 타입 변수로 넘긴다.
  3. 제네릭 함수는 자신의 타입 변수 T를 제네릭 인터페이스의 타입 변수 쪽으로 넘긴다.

 

제네릭 타입도 타입 추론이 가능하다.

// 1번
interface IValuable<T> {
  value: T
}

// 2번
class Valuable<T> implements IValuable<T> {
  constructor(public value: T) {}
}

// 3번
const printValue = <T>(o: IValuable<T>): void => console.log(o.value)

printValue(new Valuable<number>(1))           // 1
printValue(new Valuable<string>('hello'))     // hello
printValue(new Valuable<boolean>(true))       // true
printValue(new Valuable<number[]>([1, 2, 3])) // [1, 2, 3]
printValue(new Valuable(['hello', 'world']))  // ['hello', 'world']

 


10-2 제네릭 타입 제약

제네릭 타입 제약은 타입 변수에 적용할 수 있는 타입의 범위를 한정하는 기능이다.

원하지 않는 속성에 접근하는 것을 막거나 특정 속성만 접근할 수 있다.

<최종 타입1 extends 타입1, 최종 타입2 extends 타입2>(a: 최종 타입1, b: 최종 타입2, ...) => { }
interface LengthCheck {
  length: number
}

function logText1<T>(text: T): T {
  console.log(text.length) // Property 'length' does not exist on type 'T'.
  return text
}

function logText2<T extends LengthCheck>(text: T): T {
  console.log(text.length)
  return text
}

logText2(10)        // Argument of type 'number' is not assignable to parameter of type 'LengthCheck'.
logText2([1, 2, 3]) // 3

 

new 타입 제약

팩토리 함수는 new 연산자를 사용해 객체를 생성하는 기능을 하는 함수이다. 보통 팩토리 함수는 객체를 생성하는 방법이 복잡해 이를 단순화하려는 목적으로 구현한다.

타입스크립트 컴파일러는 '타입의 타입'을 허용하지 않아 오류가 발생하기 때문에 '타입의 타입' 구문을 만들기보다 new() 메서드 형태로 표현해야 한다.

// This expression is not constructable. Type '{}' has no construct signatures.
const create1 = <T>(type: T): T => new type()

const create2 = <T extends {new(): T}>(type: T): T => new type()
const create3 = <T>(type: new() => T): T => new type()

 

결론적으로, {new(): T}와 new() => T는 같은 의미이다

new 연산자를 type에 적용하면서 type의 생성자 쪽으로 매개변수를 전달해야 할 때 아래처럼 구문을 사용한다.

const create = <T>(type: {new(...args: any[]): T}, ...args: any[]): T => new type(...args)

class Point {constructor(public x: number, public y: number) {}}
[
  create(Date),        // 2023-07-07T06:39:46.976Z
  create(Point, 0, 0), // Point { x: 0, y: 0 }
].forEach(s => console.log(s))

 

인덱스 타입 제약

keyof 연산자는 객체의 일부 속성들만 추려서 단순한 객체를 만들 때 사용한다.

keyof T 형태로 타입 제약을 설정할 수 있고, keys 값을 넘겨줄 수 있다.

<T, K extends keyof T>
const pick = <T, K extends keyof T>(obj: T, keys: K[]) =>
  keys.map(key => ({[key]: obj[key]}))
    .reduce((result, value) => ({...result, ...value}), {})

const obj = {name: 'Jane', age: 22, city: 'Seoul', country: 'Korea'}

console.log(pick(obj, ['name', 'age']))   // { name: 'Jane', age: 22 }

//Type '"name1"' is not assignable to type '"name" | "age" | "city" | "country"'. Did you mean '"name"'?
console.log(pick(obj, ['name1', 'age1']))

 


10-3 대수 데이터 타입

타입스크립트에서 대수 데이터 타입은 '합집합 타입(union type)'과 '교집합 타입(intersection type)' 두 가지 종류가 있다.

함수형 언어들은 상속에 의존하는 타입보다 대수 데이터 타입을 선호한다.

 

합집합 타입 (|)

합집합 타입은 '또는(or)'의 의미인 '|' 기호로 다양한 타입을 연결해서 만든 타입이다.

type NumberOrString = number | string

let ns: NumberOrString = 1
ns = 'hello'

console.log(ns) // hello

 

교집합 타입 (&)

교집합 타입은 '이고(and)'의 의미인 '&' 기호로 다양한 타입을 연결해서 만든 타입이다.

type INameale = { name: string }
type IAgeable = { age: number }

const NameAndAge: INameale & IAgeable = {name: 'Jane', age: 22}
console.log(NameAndAge) // { name: 'Jane', age: 22 }

// type '{ name: string; }' is not assignable to type 'INameale & IAgeable'. Property 'age' is missing in type '{ name: string; }' but required in type 'IAgeable'.
const NameAndAge1: INameale & IAgeable = {name: 'Jane'}

 

합집합 타입 구분하기

아래와 같은 코드가 있다고 가정했을 때, shape이 어떤 타입의 객체인지 구분할 수 없기 때문에 합집합 타입의 각각을 구분할 수 있게 하는 '식별 합집합' 구문을 제공한다.

interface ISquare {size: number}
interface IRectangle {width: number, height: number}
interface ICircle {radius: number}

type IShape = ISquare | IRectangle | ICircle

const calcArea= (shape: IShape): number => {
  // shape 객체가 ISquare | IRectangle | ICircle 3가지 중에 무엇인지 알 수 없음
  return 0
}

 

식별 합집합 구문

합집합 타입을 구성하는 인터페이스들은 모두 똑같은 이름의 속성을 가지고 있어야 한다.

calcArea 함수의 switch문을 보면 tag 공통 속성이 있다. 공통 속성이 없으면 각각의 타입을 구분할 방법이 없다.

interface ISquare {tag: 'square', size: number}
interface IRectangle {tag: 'rectangle', width: number, height: number}
interface ICircle {tag: 'circle', radius: number}

type IShape = ISquare | IRectangle | ICircle

const calcArea = (shape: IShape): number => {
  switch(shape.tag) {
    case 'square': return shape.size * shape.size
    case 'rectangle': return shape.width * shape.height
    case 'circle': return Math.PI * shape.radius * shape.radius
  }
}

const square: ISquare = {tag: 'square', size: 10}
const rectangle: IRectangle = {tag: 'rectangle', width: 4, height: 2}
const circle: ICircle = {tag: 'circle', radius: 3}

console.log(calcArea(square), calcArea(rectangle), calcArea(circle)) // 100 8 28.274333882308138

 


10-4 타입 가드

타입 가드란, 데이터의 타입을 알 수 없거나 될 수 있는 타입이 여러 가지일 경우, 조건문을 통해서 타입을 좁혀나가는 것을 의미한다. 또한 타입을 변환하지 않은 코드 때문에 프로그램이 비정상으로 종료되는 상황을 보호해 준다.

조건문에서 typeof 또는 instanceof를 사용하면, 타입스크립트는 해당 조건문 블록 내에서는 해당 변수의 타입이 다르다는 것(= 좁혀진 범위의 타입)을 이해할 수 있다.

 

합집합 타입으로 FlyOrSwim 함수를 만들면 타입이 Bird인지 Fish인지 알 수 없기 때문에 오류가 발생하고, 이를 방지하기 위해서는 타입 가드를 사용해야 한다.

class Bird {fly() {console.log('fly')}}
class Fish {swim() {console.log('swim')}}

const FlyOrSwim = (o: Bird | Fish): void => {
  // Property 'fly' does not exist on type 'Bird | Fish'. Property 'fly' does not exist on type 'Fish'.
  o.fly()
}

 

instanceof 연산자

instanceof 연산자는 2개의 피연산자가 필요하다.

객체 instanceof 타입 // boolean 타입의 값 반환
class Bird {fly() {console.log('fly')}}
class Fish {swim() {console.log('swim')}}

const FlyOrSwim = (o: Bird | Fish): void => {
  if(o instanceof Bird) {
    o.fly()
  } else if(o instanceof Fish) {
    o.swim()
  }
}

[new Bird, new Fish].forEach(FlyOrSwim) // fly swim

 

is 연산자를 활용한 사용자 정의 타입 가드 함수 제작

instanceof처럼 타입 가드 기능을 하는 함수를 만들기 위해서는 함수의 반환 타입 부분에 is 연산자를 사용해야 한다.

변수 is 타입
class Bird {fly() {console.log('fly')}}
class Fish {swim() {console.log('swim')}}

const isFlyable = (o: Bird | Fish): o is Bird => {
  return o instanceof Bird
}

const isSwimmable = (o: Bird | Fish): o is Fish => {
  return o instanceof Fish
}

const FlyOrSwim = (o: Bird | Fish) => {
  if(isFlyable(o)) {
    o.fly()
  } else if(isSwimmable(o)) {
    o.swim()
  }
}

[new Bird, new Fish].forEach(FlyOrSwim) // fly swim

 

typeof 연산자

typeof 연산자는 객체 데이터를 객체 타입으로 변환해 준다.

 

charAt(): 문자열에서 지정된 위치에 있는 문자 반환

valueAt(): 넘버가 가지고 있는 값 반환

function doSomething(x: string | number) {
    if(typeof x === 'string') {
        console.log(x.charAt(0))
    } else if(typeof x === 'number') {
        console.log(x.valueOf())
    }
}

doSomething('abc')     // a
doSomething(10)        // 10
doSomething([1, 2, 3]) // Argument of type 'number[]' is not assignable to parameter of type 'string | number'.

 


10-5 F-바운드 다형성

this 타입과 F-바운드 다형성

타입스크립트에서 this 키워드는 타입으로 사용된다.

this가 타입으로 사용되면 객체지향 언어에서 의미하는 다형성(polymorphism) 효과가 나타나고, 일반적인 다형성과 구분하기 위해 this 타입으로 인한 다형성을 'F-바운드 다형성(F-bound polymorphism)' 이라고 한다.

 

(1) F-바운드 타입

F-바운드 타입이란, 자신을 구현하거나 상속하는 서브타입(subtype)을 포함하는 타입을 말한다.

// 특별히 자신을 상속하는 타입이 포함되어 있지 않은 일반 타입
interface IValueProvider<T> {
  value(): T
}

// add 메서드가 내가 아닌 나를 상속하는 타입을 반환하는 F-바운드 타입
interface IAddable<T> {
  add(value: T): this
}

// 메서드의 반환 타입이 this 이므로 F-바운드 타입
interface IMultiplyable<T> {
  multiply(value: T): this
}

 

(2) IValueProvider<T> 인터페이스의 구현

_value 속성을 private으로 만들어 Calculator를 사용하는 코드에서 _value 속성이 아닌 value() 메서드로 접글할 수 있게 설계되었다.

// F-바운드 타입이 아닌 일반 타입의 인터페이스 구현
class Calculator implements IValueProvider<number> {
  constructor(private _value: number = 0) {}
  value(): number {return this._value}
}

class StringComposer implements IValueProvider<string> {
  constructor(private _value: string = '') {}
  value(): string{return this._value}
}

 

(3) IAddable<T>와 IMultiplyable<T> 인터페이스 구현

add 메서드는 클래스의 this 를 반환하는데 이는 메서드의 체인 기능을 구현하기 위함이다.

같은 방법으로 multiply 메서드를 구현한다.

class Calculator implements IValueProvider<number>, IAddable<number>, IMultiplyable<number> {
    constructor(private _value: number = 0) { }
    value(): number {return this._value}

    // 클래스의 this 를 반환 (메서드 체인 기능을 구현하기 위함)
    add(value: number): this {
        this._value = this._value + value
        return this
    }

    multiply(value: number): this {
        this._value = this._value * value
        return this
    }
}

 

결과 

import 시키지 않은 전체 코드

// 특별히 자신을 상속하는 타입이 포함되어 있지 않은 일반 타입
interface IValueProvider<T> {
  value(): T
}

// add 메서드가 내가 아닌 나를 상속하는 타입을 반환하는 F-바운드 타입
interface IAddable<T> {
  add(value: T): this
}

// 메서드의 반환 타입이 this 이므로 F-바운드 타입
interface IMultiplyable<T> {
  multiply(value: T): this
}

class Calculator implements IValueProvider<number>, IAddable<number>, IMultiplyable<number> {
  constructor(private _value: number = 0) { }
  value(): number {return this._value}

  // 클래스의 this 를 반환 (메서드 체인 기능을 구현하기 위함)
  add(value: number): this {
    this._value = this._value + value
    return this
  }

  multiply(value: number): this {
    this._value = this._value * value
    return this
  }
}

const value = (new Calculator(1))
    .add(2) // 3
    .add(3) // 6
    .multiply(4) // 24
    .value()

console.log(value) // 24

 


10-6 nullable 타입과 프로그램 안전성

nullable 타입이란?

자바스크립트와 타입스크립트는 변수가 초기화되지 않으면 undefined 이라는 값을 기본으로 지정한다.

타입스크립트에서 undefine 값의 타입은 undefined이고, null 값의 타입은 null이다. 하지만 이 둘은 사실상 같은 것이므로 서로 호환되고, undefined 변수에 null 값을 지정할 수 있고, null 변수에 undefined 값을 지정할 수 있다.

즉, undefined 타입 + null 타입을 nullable 타입이라고 한다. 

 

그러나 nullable 타입은 프로그램이 동작할 때 프로그램을 비정상으로 종료시키는 주요 원인이 되기 때문에 프로그램의 안전성을 헤치게 된다.

 

주의: tsconfig.json 의 strict: true로 설정 시에는 호환 안됨

let u: undefined = undefined
let n: null = null

u = null
n = undefined

console.log(u) // null
console.log(n) // undefined

 

type nullable = undefined | null

const nullable: nullable = undefined

 

옵션 체이닝 연산자 (?.)

변수가 선언만 되었을 뿐 어떤 값으로 초기화되지 않으면 코드를 작성할 때는 문제가 없지만, 실제로 코드를 실행하면 런타임 오류가 발생해 프로그램이 비정상으로 종료한다.

이런 오류는 프로그램의 안전성을 해치므로 '옵션 체이닝' 연산자나 '널 병합 연산자'를 제공한다.

 

자바스크립트는 옵션 체이닝인 ?. 연산자를 표준으로 채택했고, 타입스크립트는 버전 3.7.2부터 사용가능하다.

interface IPerson {
  name: string
  age?: number
}

let person: IPerson

console.log(person.name)  // 런타임 오류, TypeError: Cannot read properties of undefined (reading 'name')
console.log(person?.name) // undefined
type ICoordinates = {longitude: number}
type ILocation = {country: string, coords?: ICoordinates}
type IPerson = {name: string, location?: ILocation}

let person: IPerson = {name: 'Jack'}
let longitude = person?.location?.coords?.longitude

console.log(longitude) // undefined

 

널 병합 연산자 (??)

자바스크립트는 널 병합 연산자인 ?? 연산자를 표준으로 채택했고, 타입스크립트는 버전 3.7.2부터 사용가능하다.

옵션 체이닝 연산자 부분이 undefined가 되면 널 병합 연산자가 동작해 0을 반환한다.

type ICoordinates = {longitude: number}
type ILocation = {country: string, coords?: ICoordinates}
type IPerson = {name: string, location?: ILocation}

let person: IPerson = {name: 'Jack'}
let longitude = person?.location?.coords?.longitude ?? 0

console.log(longitude) // 0

 

nullable 타입의 함수형 방식 구현

Option 타입 객체는 Option.Some(값) 또는 Option.None 형태로만 생성할 수 있다.

import {Some} from './Some'
import {None} from './None'

class Option {
  private constructor() {}
  static Some<T>(value: T) {return new Some<T>(value)}
  static None = new None()
}

let o: Option = Option.Some(1)
let n: Option = Option.None()

 

(1) Some 클래스 구현

Some 클래스의 사용자는 항상 getOrElse 메서드를 통해 Some 클래스에 담긴 값을 얻어야 한다.

Some 클래스의 사용자는 value 값을 변경하려면 항상 map 메서드를 사용해야 한다.

interface IValuable<T> {
  getOrElse(defaultValue: T)
}

interface IFunctor<T> {
  map<U>(fn: (value: T) => U)
}

class Some<T> implements IValuable<T>, IFunctor<T> {
  constructor(private value: T) {}
  
  getOrElse(defaultValue: T) {
    return this.value ?? defaultValue
  }
  
  map<U>(fn: (T) => U) {
    return new Some<U>(fn(this.value))
  }
}

 

(2) None 클래스 구현

None 클래스는 nullable 타입의 값을 의미하므로, nullable 값들이 map의 콜백 함수에 동작하면 프로그램이 비정상으로 종료될 수 있다.

interface IValuable<T> {
  getOrElse(defaultValue: T)
}

interface IFunctor<T> {
  map<U>(fn: (value: T) => U)
}

type nullable = undefined | null

class None implements IValuable<nullable>, IFunctor<nullable> {
  getOrElse<T>(defaultValue: T | nullable) {
    return defaultValue
  }
  
  map<U>(fn: (T) => U) {
    return new None
  }
}

 

(3) Some과 None 클래스 사용

Some 타입에 설정된 값 1은 map 메서드를 통해 2로 바뀌었고, getOrElse 메서드에 의해 value 변수에 2가 저장됐다.

 

None 타입 변수 n 은 map 메서드를 사용할 수는 있지만 이 map 메서드의 구현 내용은 콜백 함수를 실행하지 않고 단순히 None 타입 객체만 반환하기 때문에 getOrElse(0) 메서드가 호출되어 전달받은 0 이 저장됐다.

 

FIXME:: @ts-ignore 없으면 오류남

// @ts-ignore
class Option {
  private constructor() {}
  static Some<T>(value: T) {return new Some<T>(value)}
  static None = new None()
}

// @ts-ignore
let m = Option.Some(1)
let value = m.map(value => value + 1).getOrElse(1)
console.log(value) // 2

// @ts-ignore
let n = Option.None
value = n.map(value => value + 1).getOrElse(0)
console.log(value) // 0

 

Option 타입과 예외 처리

Option 타입은 부수 효과가 있는 불순 함수를 순수 함수로 만드는데 효과적이다.

 

아래 코드는 parseInt 의 반환값이 NaN 인지에 따라 Option.None 혹은 Option.Some 타입의 값을 반환한다.

import {IFunctor} from './option/IFunctor'
import {IValuable} from './option/IValuable'
import {Option} from './option/Option'

const parseNumber = (n: string): IFunctor<number> & IValuable<number> => {
    const value = parseInt(n)
    return isNaN(value) ? Option.None : Option.Some(value)
}

 

값이 정상 변환되면 map 메서드가 동작하여 4가 출력되고, 비정상이면 getOrElse(0) 이 동작해 0 이 리턴된다.

import {IFunctor} from './option/IFunctor'
import {IValuable} from './option/IValuable'
import {Option} from './option/Option'

const parseNumber = (n: string): IFunctor<number> & IValuable<number> => {
    const value = parseInt(n)
    return isNaN(value) ? Option.None : Option.Some(value)
}

let value = parseNumber('1')
    .map(value => value + 1) // 2
    .map(value => value * 2) // 4
    .getOrElse(0)
console.log(value) // 4

value = parseNumber('hello')
    .map(value => value + 1) // 콜백 함수 호출안됨
    .map(value => value * 2) // 콜백 함수 호출안됨
    .getOrElse(0)
console.log(value) // 0

 

JSON 포맷 문자열에 따라 예외를 발생시키는 부수 효과가 있는 불순 함수를 try/catch 문과 함께 Option 을 활용하여 순수 함수로 전환시킨 예시이다.

import {IFunctor} from './option/IFunctor'
import {IValuable} from './option/IValuable'
import {Option} from './option/Option'

const parseJeon = <T>(json: string): IValuable<T> & IFunctor<T> => {
    try {
        const value = JSON.parse(json)
        return Option.Some<T>(value)
    } catch (e) {
        return Option.None
    }
}

const json = JSON.stringify({name: 'Jack', age: 32})
let value = parseJeon(json).getOrElse({})
console.log(value)  // { name: 'Jack', age: 32 }

value = parseJeon('hello').getOrElse({})
console.log(value) // {}

 

 

 

참고자료

Do it! 타입스크립트 프로그래밍

목표

  • 수정 페이지 만들기
  • 게시글 삭제 버튼 만들기
  • 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별로 각각의 상세 페이지를 보여줄 수 있도록 만들었다.

https://ramdajs.com/docs/

람다 라이브러리에 대한 자세한 함수 사용법은 공식문서를 참고한다.

아래 글은 책의 내용 중, 중요한 부분만 간략하게 정리했다.

 

09-1 람다 라이브러리 소개

ramda 패키지는 compose나 pipe를 사용하는 함수 조합을 쉽게 할 수 있게 설계된 오픈소스 자바스크립트 라이브러리이다.

람다 라이브러리는 순수 함수를 고려해 설계었고, 람다 라이브러리가 제공하는 함수들은 항상 입력 변수의 상태를 변화시키지 않고 새로운 값을 반환한다.

 

  • 타입스크립트 언어와 100% 호환
  • compose와 pipe 함수 제공
  • 자동 커리 기능 제공
  • 포인트가 없는 고차 도움 함수 제공
  • 조합 논리 함수 일부 제공
  • 하스켈 렌즈 라이브러리 기능 일부 제공
  • 자바스크립트 표준 모나드 규격과 호환

 


09-2 람다 기본 사용법

R.compose 함수

R.compose 함수를 사용해 함수를 조합한다.

import * as R from 'ramda'

const str = R.toUpper('a')

R.compose(
  R.tap(n => console.log(n))
)(str)

// A

 

R.pipe 함수

R.tap 디버깅용 함수는 2차 함수 형태로 현재 값을 파악할 수 있게 해 준다.

R.pipe 함수를 사용해 함수를 조합한다.

import * as R from 'ramda'

const array: number[] = R.range(1, 10)

R.pipe(
  R.tap(n => console.log(n))
)(array)

// [1, 2, 3, 4, 5, 6, 7, 8, 9]

 

자동 커리 이해하기

매개변수가 2개인 일반 함수처럼 사용할 수도 있고,

2차 고차 함수로 만들어 사용할 수도 있다.

import * as R from 'ramda'

console.log(
  R.add(1, 2), // 3
  R.add(1)(3)  // 4
)

 


09-3 배열에 담긴 수 다루기

사칙 연산 함수

import * as R from 'ramda'

console.log(R.add(1)(2))       // 3
console.log(R.subtract(1)(2))  // -1
console.log(R.multiply(1)(2))  // 2
console.log(R.divide(1)(2))    // 0.5

 


09-4 서술자와 조건 연산

수의 크기를 판단하는 서술자

lt : a < b 이면 true

lte : a<=b 이면 true

gt : a>b 이면 true

gte : a>=b 이면 true

import * as R from 'ramda'

console.log(R.lt(1)(2))  // true
console.log(R.lte(1)(2)) // true

console.log(R.gt(1)(2))  // false
console.log(R.gte(1)(2)) // false
import * as R from 'ramda'

R.pipe(
  R.filter(R.lte(3)),
  R.tap(n => console.log(n))
)(R.range(1, 10))

// [3, 4, 5, 6, 7, 8, 9]

 

R.ifElse 함수

조건 서술자, true일 때 실행할 함수, false일 때 실행할 함수 구조로 되어있다.

import * as R from 'ramda'

const substractOrAdd = R.pipe(
  R.map(R.ifElse(
    R.lte(5), // a<=b
    R.inc,
    R.dec
  )),
  R.tap(n => console.log(n))
)

const result1 = substractOrAdd([1, 2, 3]) // [0, 1, 2]
const result2 = substractOrAdd([6, 7, 8]) // [7, 8, 9]

 


09-5 문자열 다루기

구분자를 사용해 문자열을 배열로 전환

import * as R from 'ramda'

console.log(R.split(' ')(`Hello World`))  // [ 'Hello', 'World' ]
console.log(R.join(' ')(['Hello World'])) // Hello World

 


09-6 chance 패키지로 객체 만들기

chance 패키지는 가짜 데이터를 만들어주는 라이브러리이다.

// @ts-ignore
import Chance from 'chance'

const c = new Chance

console.log(c.animal())   // Pigs and Hogs
console.log(c.color())    // #236e2d
console.log(c.sentence()) // Egoemu buzok el cawefwu vipur kokjuwcek ko hinhelci seal ho deunuda amuw fuvaer urbo ju.

 


09-7 렌즈를 활용한 객체의 속성 다루기

렌즈란?

렌즈(lens)는 하스켈 언어의 Control.Lens 라이브러리 내용 중 자바스크립트에서 동작할 수 있는 게터(getter)와 세터(setter) 기능만을 람다 함수로 구현한 것이다.

복잡한 객체(depth가 깊은 구조)를 다룰 때 유용하다.

 

R.prop 함수

특정 속성값을 가지고 오고, 이를 게터(getter)라고 한다.

import * as R from 'ramda'

const person = {name: '보라돌이'}

const name = R.pipe(
  R.prop('name'),
  R.tap(name => console.log(name))
)(person)

// 보라돌이

 

R.assoc 함수

특정 속성값을 변경하고, 이를 세터(setter)라고 한다.

import * as R from 'ramda'

const person = {name: '보라돌이'}

const name = R.pipe(
  R.assoc('name', '뚜비'),
  R.tap(name => console.log(name))
)(person)

// { name: '뚜비' }

 

R.lens 함수

타입스크립트에서 동작하게 하려면 제네릭 타입으로 선언해주어야 한다.

import * as R from 'ramda'

const xLens = R.lens(R.prop('x'), R.assoc('x'))

R.view(xLens, {x: 1, y: 2})            // 1
R.set(xLens, 3, {x: 1, y: 2})          // {x: 3, y: 2}
R.over(xLens, R.negate, {x: 1, y: 2})  // {x: -1, y: 2}

 

R.lensIndex 함수

지정된 인덱스 값을 반환한다.

import * as R from 'ramda'

const headLens = R.lensIndex(0)

R.view(headLens, ['a', 'b', 'c'])            // 'a'
R.set(headLens, 'x', ['a', 'b', 'c'])        // ['x', 'b', 'c']
R.over(headLens, R.toUpper, ['a', 'b', 'c']) // ['A', 'b', 'c']

 

R.lensPath 함수

지정된 경로의 값을 반환한다.

import * as R from 'ramda'

const xHeadYLens = R.lensPath(['x', 0, 'y']);

R.view(xHeadYLens, {x: [{y: 2, z: 3}, {y: 4, z: 5}]})           // 2
R.set(xHeadYLens, 1, {x: [{y: 2, z: 3}, {y: 4, z: 5}]})         // {x: [{y: 1, z: 3}, {y: 4, z: 5}]}
R.over(xHeadYLens, R.negate, {x: [{y: 2, z: 3}, {y: 4, z: 5}]}) // {x: [{y: -2, z: 3}, {y: 4, z: 5}]}

 

R.lensProp 함수

지정된 속성의 값을 반환한다.

import * as R from 'ramda'

const xLens = R.lensProp('x')

R.view(xLens, {x: 1, y: 2})            // 1
R.set(xLens, 4, {x: 1, y: 2})          // {x: 4, y: 2}
R.over(xLens, R.negate, {x: 1, y: 2})  // {x: -1, y: 2}

 


09-8 객체 다루기

R.key와 R.value 함수

import * as R from 'ramda'

const obj = {name: 'Jae', age: 10}

const key = R.keys(obj)
const value = R.values(obj)

console.log(key)   // [ 'name', 'age' ]
console.log(value) // [ 'Jae', 10 ]

 


09-9 배열 다루기

R.map 함수

import * as R from 'ramda'

const array = [1, 2, 3]

console.log(R.map(item => item * 2, array)) // [ 2, 4, 6 ]

 

R.filter 함수

import * as R from 'ramda'

const array = [1, 2, 3]

console.log(R.filter(item => item % 2 === 0, array)) // [2]

 

R.sort 함수

import * as R from 'ramda'

const sortArr = R.sort((a: number, b: number): number => a - b)

console.log(sortArr([3, 5, 1, 7, 9])) // [ 1, 3, 5, 7, 9 ]

 

R.sortBy 함수

import * as R from 'ramda'

const persons = [
  { name: '보라돌이', age: 10},
  { name: '뚜비', age: 5},
  { name: '나나', age: 15},
  { name: '뽀', age: 1},
]

const nameSort = R.sortBy(R.prop('name'))(persons)
const ageSort = R.sortBy(R.prop('age'))(persons)

console.log(nameSort)
// [
  { name: '나나', age: 15 },
  { name: '뚜비', age: 5 },
  { name: '보라돌이', age: 10 },
  { name: '뽀', age: 1 }
]

console.log(ageSort)
// [
  { name: '뽀', age: 1 },
  { name: '뚜비', age: 5 },
  { name: '보라돌이', age: 10 },
  { name: '나나', age: 15 }
]

 


09-10 조합 논리 이해하기

조합자란?

조합 논리학은 '조합자(combinator)'라는 특별한 형태의 고차 함수들을 결합해 새로운 조합자를 만들어 내는 것입니다.

 

R.chain 함수

import * as R from 'ramda'

const arr = [1, 2, 3]

const chain = R.pipe(
  R.chain(n => [n, n])
)(arr)

console.log(chain) // [ 1, 1, 2, 2, 3, 3 ]

 

 

 

참고자료

Do it! 타입스크립트 프로그래밍

+ Recent posts