목표

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

 


결과 화면

 


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

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

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

 

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

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

 

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

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

 

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

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

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

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

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

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

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

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

 

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

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

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

 

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

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

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

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

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

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

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

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

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

export default BoardList

 


전체 코드

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

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

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

  const boardLength = boardList.length

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

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

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

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

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

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

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

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

        <tbody>
          {
            currentPost.map((board, index) => {
              return (
                <tr key={index}>
                  <td>{index + 1}</td>
                  <td className="title"><Link to={`/board/${board.id}`}>{board.title}</Link></td>
                  <td>{dayjs(board.created_at).format('YYYY.MM.DD')}</td>
                </tr>
              )
            })
          }
        </tbody>
      </table>

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

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

export default BoardList

 

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

목표

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

 


결과 화면

 


1. axios 패키기 설치하기

axios 패키기를 설치한다.

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

yarn add axios
또는
npm install axios

 

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

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

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

 

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

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

 

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

 

 

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

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

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

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

export default BoardList

 

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

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

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

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

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

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

 

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

dayjs 패키지를 설치한다.

yarn add dayjs
또는 
npm install dayjs

 

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

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

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

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

  return (
    <div className="board-list">
        ...
        <tbody>
          {
            boardList.map((board, index) => {
              return (
                <tr key={index}>
                  <td>{index + 1}</td>
                  <td className="title">
                    <Link to={`/board/${board.id}`}>{board.title}</Link>
                  </td>
                  
                  // 수정한 부분
                  <td>{dayjs(board.created_at).format('YYYY.MM.DD')}</td>
                </tr>
              )
            })
          }
        </tbody>
      </table>
    </div>
  )
}

 


전체 코드

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

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

  const boardLength = boardList.length

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

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

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

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

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

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

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

export default BoardList

 

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

목표

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

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

 


결과 화면

 


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

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

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

 

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

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

 

 

2. 게시판 폴더 구조

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

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

 

 

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

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

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

 

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

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

 

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

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

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

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

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

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

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

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

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

export default BoardList

 

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

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

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

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

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

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

        &.title {
          text-align: left;

          a {
            padding-bottom: 2px;

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

 

4. 라우터 연결하기

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

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

 

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

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

 

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

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

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

 

  • src/App.tsx

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

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

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

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

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

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

export default App

 

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

08-1 함수형 프로그래밍이란?

함수형 프로그래밍은 프로그램이 상태의 변화 없이 데이터 처리를 수학적 함수 계산으로 취급하고자 하는 패러다임이다.

 

함수형 프로그래밍은 1. 순수 함수, 2. 함수 조합, 3. 모나드 조합으로 코드를 설계하고 구현하는 기법이고, 다음 3가지 수학 이론에 기반을 두고 있다.

  1. 람다 수학: 조합 논리와 카테고리 이론의 토대가 되는 논리 수학
  2. 조합 논리: 함수 조합의 이론적 배경
  3. 카테고리 이론: 모나드 조합과 고차 타입의 이론적 배경

 

명령형 프로그래밍 vs 함수형(선언형) 프로그래밍
  • 명령형 프로그래밍
const name = 'Jae'
console.log(name) // Jae

 

  • 함수형 프로그래밍
function callName(name) {
    return name
}

console.log(callName('Jae')) // Jae

 


08-2 제네릭 함수

타입스크립트의 함수는 매개변수와 반환값에 타입이 존재하므로, 함수 조합을 구현할 때는 제네릭 함수 구문을 사용해야만 한다.

 

타입스크립트의 제네릭 함수 구문

타입스크립트에서 제네릭 타입은 함수, 인터페이스, 클래스, 타입 별칭에 적용할 수 있고, <T>, <T, Q>로 표현한다.

제네릭 타입으로 함수를 정의하면 어떤 타입에도 대응할 수 있다.

// function 함수
function g1<T>(a: T): void {}
function g2<T, Q>(a: T, b: Q): void {}

// 화살표 함수
const g3 = <T>(a: T): void => {}
const g4 = <T, Q>(a: T, b: Q): void => {}

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

 

함수의 역할

프로그래밍 언어로 수학의 함수를 구현할 때는 변수의 타입을 고려해야 한다.

타입스크립트 언어로 일대일 맵 함수를 만든다면 타입 T인 값을 이용해 타입 R 값을 만들어 주어야 한다.

type MapFunc<T, R> = (T) => R

 

아이덴티티 함수

아이덴티티 함수는 입력과 출력의 타입이 같은 함수이다.

type MapFunc<T, R> = (T) => R
type IdentityFunc<T> = MapFunc<T, T>

const numberIdentity: IdentityFunc<number> = (x: number): number => x
const stringIdentity: IdentityFunc<string> = (x: string): string => x
const objectIdentity: IdentityFunc<object> = (x: object): object => x
const arrayIdentity: IdentityFunc<any[]> = (x: any[]): any[] => x

 


08-3 고차 함수와 커리

함수에서 매개변수의 개수를 에리티(arity)라고 한다.

f()는 에리티가 0개인 함수, f(x, y)는 에리티가 2개인 함수이다.

 

함수형 프로그래밍에서 compose나 pipe라는 이름의 함수를 사용해 compose(h, g, f) 또는 pipe(f, g, h) 형태로 함수들을 조합해 새로운 함수를 만들 수 있다.

 

고차 함수란?

타입스크립트에서 함수는 변수에 담긴 함수 표현식이고, 함수의 반환값으로 함수를 사용할 수 있다.

고차함수는 어떤 함수가 또 다른 함수를 반환하는 함수를 의미한다.

export type FirstFunc<T, R> = (T) => R
export type SecondFunc<T, R> = (T) => FirstFunc<T, R>
export type ThirdFunc<T, R> = (T) => SecondFunc<T, R>

 

부분 적용 함수와 커리

부분 적용 함수 또는 부분 함수는 자신의 차수보다 함수 호출 연산자를 덜 사용하는 함수이다.

 

클로저

고차 함수의 몸통에서 선언되는 변수들은 클로저(closer)라는 유효 범위를 가진다.

function add(x: number): (y: number) => number {
  return function(y: number): number {
    // 클로저
    return x + y
  }
}

// 변수 x 메모리 유지
const add1 = add(1)
console.log(add1) // [Function (anonymous)]

// result에 3을 저장 후 변수 x 메모리 해제
const result = add1(2)
console.log(result) // 3

 


08-4 함수 조합

함수 조합은 작은 기능을 구현한 함수를 여러 번 조합해 더 의미 있는 함수를 만들어 내는 프로그램 설계 기법이다.

함수 조합을 할 수 있는 언어들은 composer 혹은 pipe 라는 이름의 함수를 제공하거나 만들 수 있다.

 

composer, pipe 함수는 자바스크립트에 존재하는 함수가 아닌 함수 패턴으로 직접 구현해야 한다.

 

compose 함수

  1. compose 함수는 가변 인수 스타일로 함수들의 배열을 입력받는다. (...functions)
  2. compose 함수는 인자로 받은 함수 리스트를 오른쪽에서 왼쪽으로 실행하기 때문에 reverse() 한다.
export const compose = <T, R>(...functions: readonly Function[]): Function => (x: any): (T: any) => R => {
  const deepCopiedFunctions = [...functions]
  return deepCopiedFunctions.reverse().reduce((value, func) => func(value), x)
}

const inc = (x: number): number => x + 1
const composed = compose(inc, inc, inc)

console.log(composed(1)) // 4

 

pipe 함수

  1. pipe 함수는 가변 인수 스타일로 함수들의 배열을 입력받는다. (...functions)
  2. pipe 함수는 인자로 받은 함수 리스트를 왼쪽에서 오른쪽으로 실행하기 때문에 reverse() 코드가 필요 없다.

 

pipe 함수는 compose와 매개변수들을 해석하는 순서다 반대이므로, functions을 reverse() 하는 코드가 없다.

위의 compose 함수와 결과가 동일하다.

export const pipe = <T>(...functions: Function[]): Function => (x: T): T => {
  return functions.reduce((value, func) => func(value), x)
}

const inc = (x: number): number => x + 1
const composed = pipe(inc, inc, inc)

console.log(composed(1)) // 4

 

pipe와 compose 함수 분석

  1. compose, pipe 함수는 가변 인수 방식으로 동작하므로, 매개변수를 전개 연산자로 설정한다.
export const pipe = (...functions)

 

    2. 각 함수의 시그니처가 모두 다르면 제네릭 타입을 적용하기 힘들므로,  functions의 타입은 함수들의 배열인 Function[]으로 설정한다.

export const pipe = (...functions: Function[])

 

    3. pipe 함수로 조합된 결과 함수는 애리티가 1이므로, 매개변수 x를 입력받는 함수를 작성한다.

export const pipe = <T>(...functions: Function[]): Function => (x: T) => T

 

    4. pipe 함수는 reduce() 메서드를 사용해 배열의 각 요소에 대해 주어진 함수를 실행하고, 하나의 결괏값을 반환한다.

export const pipe = <T>(...functions: Function[]): Function => (x: T): T => {
  return functions.reduce((value, func) => func(value), x)
}

 

compose 함수는 pipe 함수와 매개변수 방향이 반대이다.

즉, pipe(f, g, h) === compose(h, g, f)

 

    5. compose 함수는 매개변수를 전개 연산자로 전개한 다음, 그 내용을 깊은 복사 한다.

export const compose = <T>(...functions: readonly Function[]): Function => (x: T): T => {
  const deepCopiedFunctions = [...functions]
}

 

    6. compose 함수는 매개변수 방향이 반대이기 때문에 reverse()를 사용해 결괏값을 반환한다.

export const compose = <T>(...functions: readonly Function[]): Function => (x: T): T => {
  const deepCopiedFunctions = [...functions]
  return deepCopiedFunctions.reverse().reduce((value, func) => func(value), x)
}

 

부분 함수와 함수 조합

고차 함수의 부분 함수는 함수 조합에서 사용될 수 있다.

 

add 함수는 2차 고차 함수이다.

inc 함수는 add의 부분 함수이다.

reault 함수는 pipe 함수를 갖고, inc, add 함수를 조합해서 만든 함수이다.

export const pipe = <T>(...functions: Function[]): Function => (x: T): T => {
  return functions.reduce((value, func) => func(value), x)
}

const add = (x: number) => (y: number) => x + y
const inc = add(1)

const result = pipe(inc, add(2))

console.log(result(3)) // 6

 

포인트가 없는 함수

map 함수는 함수 조합을 고려해 설계한 것으로, map(f) 형태의 부분 함수를 만들면  compose나 pipe에 사용할 수 있다.

이처럼 함수 조합을 고려해 설계한 함수를 '포인트가 없는 함수' 라고 한다.

export const pipe = <T>(...functions: Function[]): Function => (x: T): T => {
  return functions.reduce((value, func) => func(value), x)
}

export const map = (f: any) => (a: any) => a.map(f)

const square = (value: number) => value * value
const squaredMap = map(square)
const fourSquare = pipe(squaredMap, squaredMap)

console.log(fourSquare([2, 3])) // [16, 81]

 

함수 조합은 복잡하지 않은 함수들은 compose 또는 pipe로 조합해 복잡한 내용을 쉽게 만들 수 있다.

 

 

 

참고자료

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

동기(Synchronous)

동기 방식은 서버에 요청을 보냈을 때, 응답이 돌아와야 다음 동작을 수행할 수 있다.

즉, A 작업이 끝나야 B 작업이 실행된다.

 

비동기(Asynchronous)

비동기 방식은 서버에 요청을 보냈을 때, 응답 상태와 상관없이 다음 동작을 수행할 수 있다.

즉, A 작업이 진행되면서 B 작업이 실행되고, A 작업이 끝나면 B 작업과 상관없이 결괏값이 출력된다.

 

동기 / 비동기

 

스레드

스레드란, CPU가 프로그램을 동작시키는 최소 단위이다.

운영체제에서 프로그램이 실행되고 있는 상태를 프로세스라고 하고, 프로세스는 1개의 메인 스레드와 여래 개의 작업 스레드를 동작시킨다.

웹 브라우저나 NodeJs 자체는 다중 스레드로 동작하지만, 자바스크립트(타입스크립트)는 단일 스레드로 동작한다.

 

단일 스레드, 싱글 스레드

싱글 스레드는 프로세스 내에서 하나의 메인 스레드만으로 작업을 처리한다.

즉, 작업을 순서대로 처리하고, 만약 선행 스레드의 작업이 매우 길어진다면, 후행 스레드는 선행 작업이 끝날 때까지 기다려야 한다.

 

다중 스레드, 멀티 스레드

멀티 스레드는 CPU의 최대 활용을 위해 2개 이상의 스레드를 동시에 실행시켜 작업을 처리한다.

문맥 교환(context switching)을 통해서 각 스레드의 작업을 조금씩 처리하고, 사용자의 입장에서 프로그램들이 동시에 수행되는 것처럼 보인다.

 

 


07-1 비동기 콜백 함수

동기와 비동기 API

  • 동기 버전

ex) readFileSycn, ~Sync

NodeJs에서 파일 읽기는 readFileSync라는 이름의 API를 사용해서 구현하고, Buffer라는 타입으로 전달해 준다.

Buffer는 NodeJs가 제공하는 클래스로서 바이너리 데이터를 저장하는 기능을 수행한다.

 

  • 비동기 버전

ex) readFile

readFile(파일경로, 콜백함수: (error: Error, buffer: Buffer) => void)

콜백 함수의 첫 번째 매개변수를 통해서 예외 처리를 하고, 두 번째 매개변수를 콜백 함수에 전달한다.

 

동기 방식 API는 파일 내용을 모두 읽을 때(= 작업이 종료될 때)까지 프로그램의 동작을 잠시 멈춘다.

비동기 방식 API는 프로그램의 동작을 멈추지 않는 대신 결과를 콜백 함수로 얻는다. (= 비동기 콜백 함수)

import { readFileSync, readFile } from "fs";

// 동기 방식
console.log('동기 방식 API')

const buffer: Buffer = readFileSync('./text.json') // 일시적으로 멈춤
console.log(buffer.toString())


// 비동기 방식
readFile('./text.json', (error: Error, buffer: Buffer) => {
  console.log(' 비동기 방식 API')
  console.log(buffer.toString())
})


// Promise and async/await
const readFilePromise = (filename: string): Promise<string> =>
  new Promise<string>((resolve, reject) => {
    readFile(filename, (error: Error, buffer: Buffer) => {
      if(error)
        reject(error)
      else
        resolve(buffer.toString())
    })
  });

  (async () => {
    const content = await readFilePromise('./text.json')
    console.log('Promise and async/await')
    console.log(content)
  })()

 

위의 코드의 출력 화면을 보면 코드의 작성 순서대로 출력되는 것이 아니라 동기, 비동기 처리 순서에 따라 출력이 달라짐을 알 수 있다.

 

 

단일 스레드와 비동기 API

자바스크립트와 타입스크립트는 단일 스레드로 동작하므로 될 수 있으면 readFileSync와 같은 동기 API는 사용하지 말아야 한다.

동기 API는 코드를 작성하기는 쉽지만, 결괏값이 반환될 때까지 일시적으로 멈추기 때문에 웹 브라우저에서 웹 서버로 접속되지 않는 현상이 발생하고, 프로그램의 반응성을 훼손하게 된다.

따라서 자바스크립트와 타입스크립트에서 동기 API는 사용하지 말아야 한다.

 

콜백 지옥

콜백 지옥은 콜백 함수에서 또 다른 비동기 API를 호출하는 코드를 의미한다.

함수 안에 함수 호출이 반복되면 코드의 가독성이 떨어질 뿐만 아니라 코드의 유지보수 또한 어렵기 때문에 콜백 지옥이 되지 않게끔 코드를 작성해야 한다.

import { readFile } from "fs";

// 비동기
readFile('./text.json', (err: Error, buffer: Buffer) => {
  if(err) throw err
  else {
    const content: string = buffer.toString()
    console.log(content)
  }

  // 비동기
  readFile('./text1.json', (err: Error, buffer: Buffer) => {
    if(err) throw err
    else {
      const content: string = buffer.toString()
      console.log(content)
    }
  })
})

 


07-2 Promise 이해하기

자바스크립트에서 프로미스는 Promise라는 이름의 클래스이다.

Promise 클래스를 사용하려면 new 연산자를 적용해 프로미스 객체를 만들어야 한다.

 

Promise는 비동기 작업의 최종 완료 또는 실패를 나타나는 객체이고,

Promise의 콜백 함수는 resolve와 reject라는 2개의 매개변수를 갖는다.

 

  • resolve(value): 작업이 성공적으로 끝난 경우, 그 결괏값을 value와 함께 호출
  • reject(error): 에러 발생 시 에러 객체를 나타내는 error와 함께 호출
new Promise<T>((
  resolve: (value: T) => void,
  
  reject: (error: Error) => void) => {
  // 코드 구현
  })
})

 

프로미스의 3가지 상태(states)

new Promise()로 프로미스를 생성하고 종료될 때까지 3가지의 상태를 갖는다.

  • Pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태
  • Fulfilled(이행) : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
  • Rejected(실패) : 비동기 처리가 실패하거나 오류가 발생한 상태

 

Pending(대기)

new Promise() 메서드를 호출하면 대기 상태가 된다.

new Promise(function(resolve, reject){
  ...
}

 

Fulfilled(이행)

콜백 함수의 인자 resolve를 실행하면 이행 상태가 되고, 이행 결괏값을 then()을 이용해 받을 수 있다.

new Promise(function(resolve, reject){
  resolve()
}

 

Rejected(실패)

콜백 함수의 인자 reject를 실행하면 실패 상태가 되고, 실패 결괏값을 reject()를 이용해 받을 수 있다.

new Promise(function(resolve, reject){
  reject()
}

 

 

resolve와 reject 함수

Promise 타입 객체의 then, catch,  finally 메서드를 메서드 체인 형태로 사용한다.

 

  • then

Promise에서 resolve 함수를 호출한 값은 then 메서드의 콜백 함수에 전달된다.

 

  • catch

Promise에서 reject 함수를 호출한 값은 catch 메서드의 콜백 함수에 전달된다.

 

  • finally

Promise에서 finally 함수는 항상 마지막에 호출된다.

 

import {readFile} from "fs";

export const readFilePromise = (filename: string): Promise<string> =>
  new Promise<string>((
    resolve: (value: string) => void,

    reject: (error: Error) => void) => {
      readFile(filename, (err: Error, buffer: Buffer) => {
        if (err) reject(err)
        else resolve(buffer.toString())
    })
  })

readFilePromise('./text.json')
  .then((content: string) => {
    console.log(content)
    return readFilePromise('./text1.json')
  })

  .then((content: string) => {
    console.log(content)
    return readFilePromise('.')
  })

  .catch((err: Error) => console.log('error', err.message))
  .finally(() => console.log('종료'))

 

Promise.resole 메서드

Promise.resolve(값) 형태로 호출하면 해당 '값'은 then 메서드에서 얻을 수 있다.

Promise.resolve(1)
  .then(value => console.log(value)) // 1

Promise.resolve({name: 'Jack'})
  .then(value => console.log(value)) // { name: 'Jack' }

 

Promise.reject 메서드

Promise.reject(Error 타입 객체)를 호출하면 해당 'Error 타입 객체'는 catch 메서드의 콜백 함수에서 얻을 수 있다.

Promise.reject(new Error('에러 발생'))
  .catch((err: Error) => console.log(err.message)) // 에러 발생

 

then-체인

Promise의 then 메서드를 호출할 때 사용한 콜백 함수는 값을 반환할 수 있다.

then에서 반환된 값은 또 다른 then 메서드를 호출해 값을 수신할 수 있고, then 메서드는 반환된 값이 Promise 타입이면 이를 resolve 한 값을 반환한다. reject 일 경우 catch 메서드에서 거절당한 값을 얻을 수 있다.

Promise.resolve(1)
  .then((value: number) => {
    console.log('1', value) // 1
    return Promise.resolve(true)
  })

  .then((value: boolean) => {
    console.log('2', value) // true
    return [1, 2, 3]
  })

  .then((value: number[]) => {
    console.log('3', value) // [1, 2, 3]
    return {name: 'Jack'}
  })

 

Promise.all 메서드

every 메서드는 배열의 모든 아이템이 어떤 조건을 만족하면 true를 반환한다.

const isAllTrue = (values: boolean[]) => values.every(value => value == true)

console.log(isAllTrue([true, true]))  // true
console.log(isAllTrue([true, false])) // false

 

Promise.race 메서드

some 메서드는 배열의 아이템 중 하나라도 조건을 만족하면 true를 반환한다.

const isAnyTrue = (values: boolean[]) => values.some(value => value == true)

console.log(isAnyTrue([true, false]))  // true
console.log(isAnyTrue([false, false])) // false

 


07-3 async와 await 구문

2013년 마이크로소프트는 C# 5.0을 발표하면서 비동기 프로그래밍 코드를 비약적으로 간결하게 구현할 수 있는 async/await라는 구문을 제공했다.

const test = async () => {
  const value = await Promise.resolve(1)
  console.log(value)
}

test() // 1

 

await 키워드

await 키워드는 피연산자(operand)의 값을 반환해 준다.

피연산자가 Promise 객체이면 then 메서드를 호출해 얻은 값을 반환해 준다.

 

async 함수 수정자

await 키워드는 항상 async가 있는 함수 몸통에서만 사용할 수 있다.

 

결과 화면을 보면 함수의 순서대로 1, 1, hello, hello 모양이 아니라 함수가 마치 동시에 실행된 것처럼 1, hello, 1, hello 순서로 출력됨을 볼 수 있다.

export const test1 = async () => {
  let value = await 1
  console.log(value)

  value = await Promise.resolve(1)
  console.log(value)
}

export async function test2() {
  let value = await 'hello'
  console.log(value)

  value = await Promise.resolve('hello')
  console.log(value)
}

test1()
test2()

 

async 함수의 두 가지 성질

  1. 일반 함수처럼 사용할 수 있다.
  2. Promise 객체로 사용할 수 있다.

 

async 함수를 Promise 객체로 사용하면 test1() 함수 호출이 해소(resolve)된 다음에 test2() 함수를 호출한다.

export const test1 = async () => {
  let value = await 1
  console.log(value)

  value = await Promise.resolve(1)
  console.log(value)
}

export async function test2() {
  let value = await 'hello'
  console.log(value)

  value = await Promise.resolve('hello')
  console.log(value)
}

test1().then(() => test2())

 

async 함수가 반환하는 값의 의미

async 함수는 값을 반환할 수 있고, 이때 반환값은 Promise 형태로 변환되므로 then 메서드를 호출해 async 함수의 반환값을 얻어야 한다.

const asyncReturn = async() => {
  return [1, 2, 3]
}

asyncReturn().then(value => console.log(value)) // [ 1, 2, 3 ]

 

async 함수의 예외 처리

async 함수에서 예외가 발생하면 프로그램이 비정상적으로 종료된다.

비정상으로 종료하는 상황을 막으려면 함수 호출 방식이 아니라 함수가 반환하는 Promise 객체의 catch 메서드를 호출하는 형태로 코드를 작성해야 한다.

const asyncExpection = async() => {
  throw new Error('error1')
}

asyncExpection().catch(err => console.log(err.message)) // error1

const awaitReject = async() => {
  await Promise.reject(new Error('error2'))
}

awaitReject().catch(err => console.log(err.message)) // error2

 

async 함수와 Promise.all

  1. readFileAll 함수는 filenames에 담긴 배열을 map 메서드를 적용해 Promise[] 타입 객체로 전환한다.
  2. Promise.all 메서드를 사용해 단일 Promise 객체로 만든다.
  3. 만들어진 객체에 await 구문을 적용해 결괏값을 반환한다.
  4. readFileAll 함수를 Promise 객체로 취급해 then과 catch 메서드로 연결한다.

 이런식으로 코드를 작성해 예외가 발생하더라도 프로그램이 비정상적으로 종료하지 않도록 한다.

import {readFile} from "fs";

export const readFilePromise = (filename: string): Promise<string> =>
  new Promise<string>((
    resolve: (value: string) => void,

    reject: (error: Error) => void) => {
    readFile(filename, (err: Error, buffer: Buffer) => {
      if (err) reject(err)
      else resolve(buffer.toString())
    })
  })

const readFileAll = async(filenames: string[]) => {
  return await Promise.all(filenames.map(filename => readFilePromise(filename)))
}

readFileAll(['./text.json', './text1.json'])
  .then(([text, text1]: string[]) => {
    console.log('text', text)
    console.log('text1', text1)
  })

  .catch(err => console.log(err.message))

 

 

 

참고자료

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

폴더 구조

 


결과 화면

 


1. 패키기 설치하기

npm install @nestjs/typeorm typeorm pg

npm install -g ts-node

터미널에서 위의 명령어를 실행해 패키지를 설치한다.

ts-node 패키지가 전역으로 설치되어 있다면 추가로 설치할 필요는 없다.

 

2. typeorm.config.ts

  • src/config/typeorm.config.ts

해당 경로에 typeorm.config.ts 파일을 만든다.

로컬 또는 서버에서 만든 데이터베이스 정보를 입력한다.

import { TypeOrmModuleOptions } from '@nestjs/typeorm';

export const typeOrmConfig: TypeOrmModuleOptions = {
  type: 'postgres',
  host: '_HOST_',
  port: _DATABASE_PORT_,
  username: '_USER_NAME_',
  password: '_DATABASE_PASSWORD_',
  database: 'postgres',
  entities: [__dirname + './../**/*.entity.{js,ts}'],
  logging: true
};

 

2-1. 데이터베이스

참고, 로컬에 아래 사진처럼 PSQL 데이터베이스를 만들었다.

psql 테이블 만드는 법

 

 

3. app.module.ts에 TypeORM import 하기

@Module imports 부분에 TypeOrmModule.forRoot(typeOrmConfig) 모듈을 추가한다.

board.module.ts 파일이 생겼기 때문에 controllers와 providers가 중복이 되어 안에 리스트를 삭제한다.

import { Module } from '@nestjs/common';
import { BoardModule } from './board/board.module';
import { TypeOrmModule } from "@nestjs/typeorm";
import { typeOrmConfig } from "./config/typeorm.config";

@Module({
  imports: [BoardModule, TypeOrmModule.forRoot(typeOrmConfig)],
  controllers: [],
  providers: []
})

export class AppModule {}

 

5. board.entity.ts

  • src/board/entities/board.entity.ts

데이터베이스 엔티티를 작성한다.

데이터베이스 컬럼에 따라 엔티티는 다르다.

import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity('board_table')
export class BoardEntity extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @Column()
  content: string;

  @Column()
  created_at: Date;

  @Column()
  updated_at: Date;
}

 

5. create-board.dto.ts, update-board.dto

DTO(Data Transfer Object)는 데이터가 네트워크를 통해 전송되는 방법을 정의하는 개체이다.

Typescript 인터페이스나 간단한 클래스를 사용해서 DTO 스키마를 정의할 수 있고, 컴파일 과정에서도 엔티티를 보존하기 위해서 클래스를 선호한다.

 

  • src/board/dto/create-board.dto.ts
export class CreateBoardDto {
  id: number;
  title: string;
  content: string;
  created_at: Date;
  updated_at: Date;
}

 

  • src/board/dto/update-board.dto
import { PartialType } from '@nestjs/mapped-types';
import { CreateBoardDto } from './create-board.dto';

export class UpdateBoardDto extends PartialType(CreateBoardDto) {
  id: number;
  title: string;
  content: string;
  created_at: Date;
  updated_at: Date;
}

 

6. board.repository.ts 파일 만들기

https://typeorm.io/working-with-repository

https://orkhan.gitbook.io/typeorm/docs/custom-repository

TypeORM Custom repositories에 대한 설명의 위의 공식 문서를 참고한다.

 

  • src/board/board.repository.ts

사용자 정의 레포지토리를 만들고, create, findAll, findOne, update, remove에 해당하는 함수를 만든다. (함수명은 상관없다.)

create, save, finde 등 다양한 메소드들을 제공한다.

import { EntityRepository, Repository, DataSource } from 'typeorm';
import { BoardEntity } from './entities/board.entity';

@EntityRepository()
export class BoardRepository extends Repository<BoardEntity> {
  constructor(private readonly datasource: DataSource) {
    super(BoardEntity, datasource.createEntityManager())
  }

  // @Post()
  async createByBoard(createBoardDto): Promise<BoardEntity[]> {
    const newBoard = this.create(createBoardDto)

    return await this.save(newBoard)
  }

  // @Get()
  async findAllByBoard(): Promise<BoardEntity[]> {
    return await this.find()
  }

  // @Get(':id')
  async findOneByBoardId(id: number): Promise<BoardEntity> {
    return await this.findOne({
      where: {
        id
      }
    })
  }

  // @Patch
  async updateByBoard(id, updateBoardDto): Promise<BoardEntity> {
    await this.update(id, updateBoardDto)

    return await this.findOne({
      where: {
        id
      }
    })
  }

  // @Delete
  async removeByBoard(id): Promise<void> {
    await this.delete(id)
  }
}

 

7. board.service.ts

board.repository.ts에서 만든 함수들을 return한다.

import { Injectable } from '@nestjs/common';
import { BoardRepository } from './board.repository';
import { CreateBoardDto } from './dto/create-board.dto';
import { UpdateBoardDto } from './dto/update-board.dto';

@Injectable()
export class BoardService {
  constructor(
      private readonly boardRepository: BoardRepository,
  ) {}

  create(createBoardDto: CreateBoardDto) {
    return this.boardRepository.createByBoard(createBoardDto)
  }

  findAll() {
    return this.boardRepository.findAllByBoard()
  }

  findOne(id: number) {
    return this.boardRepository.findOneByBoardId(id)
  }

  update(id: number, updateBoardDto: UpdateBoardDto) {
    return this.boardRepository.updateByBoard(id, updateBoardDto)
  }

  remove(id: number) {
    return this.boardRepository.removeByBoard(id)
  }
}

 

8. board.controller.ts

Get, Post 등 axios 메서드를 작성한다.

import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { BoardService } from './board.service';
import { CreateBoardDto } from './dto/create-board.dto';
import { UpdateBoardDto } from './dto/update-board.dto';

@Controller('board')
export class BoardController {
  constructor(private readonly boardService: BoardService) {}

  @Post()
  create(@Body() createBoardDto: CreateBoardDto) {
    return this.boardService.create(createBoardDto);
  }

  @Get()
  findAll() {
    return this.boardService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.boardService.findOne(+id);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() updateBoardDto: UpdateBoardDto) {
    return this.boardService.update(+id, updateBoardDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.boardService.remove(+id);
  }
}

 

9. board.module.ts

  • src/board/board.module.ts

만든 컨트롤러, 서비스, 레포지토리를 연결한다.

import { Module } from '@nestjs/common';
import { BoardService } from './board.service';
import { BoardController } from './board.controller';
import { BoardRepository } from './board.repository';

@Module({
  controllers: [BoardController],
  providers: [BoardService, BoardRepository],
})
export class BoardModule {}

 

10. 불필요한 파일 삭제하기

app.controller.ts, app.service.ts 등 사용하지 않는 파일이 있다면 삭제한다.

※app.module.ts 파일은 삭제하면 안된다. (모듈을 import, export 하기 때문에)※

결과 화면

 


https://docs.nestjs.com/recipes/crud-generator

NestJS CRUD 생성기에 대한 자세한 설명은 위의 NestJs 공식 문서를 참고한다.

 

1. nest g resource folder-name

folder-name 부분에 원하는 폴더명을 작성하면 된다.

nest g resource folder-name 명령어는 NestJs의 컨트롤러, 모듈, 서비스, dto, 엔티티 등 NestJs 개발에 필요한 여러 리소스 파일들을 설치해 준다. = CRUD 생성기

 

옵션 선택이 나온다면 REST API로 선택하고, yes를 입력한다.

 

게시판을 만들기 위해서 nest g resource board 명령어를 실행해 board 폴더를 만들었다.

 

2. board.controller.ts

컨트롤러(controller)는 들어오는 요청을 처리하고 클라이언트에 응답을 반환하는 역할을 한다.

라우팅 메커니즘은 어떤 컨트롤러가 어떤 요청을 받는지 제어한다.

 

CRUD 생성기를 사용하면 @Post, @Get, @Patch, @Delete 메서드가 기본적으로 만들어진다.

import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { BoardService } from './board.service';
import { CreateBoardDto } from './dto/create-board.dto';
import { UpdateBoardDto } from './dto/update-board.dto';

@Controller('board')
export class BoardController {
  constructor(private readonly boardService: BoardService) {}

  @Post()
  create(@Body() createBoardDto: CreateBoardDto) {
    return this.boardService.create(createBoardDto);
  }

  @Get()
  findAll() {
    return this.boardService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.boardService.findOne(+id);
  }

  @Patch(':id')
  update(@Param('id') id: string, @Body() updateBoardDto: UpdateBoardDto) {
    return this.boardService.update(+id, updateBoardDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.boardService.remove(+id);
  }
}

 

3. board.service.ts

컨트롤러에 들어가는 서비스(service)이다.

CRUD 생성기를 사용하면 create, find, update, remove 메서드가 기본적으로 만들어진다.

import { Injectable } from '@nestjs/common';
import { CreateBoardDto } from './dto/create-board.dto';
import { UpdateBoardDto } from './dto/update-board.dto';

@Injectable()
export class BoardService {
  create(createBoardDto: CreateBoardDto) {
    return 'This action adds a new board';
  }

  findAll() {
    return `This action returns all board`;
  }

  findOne(id: number) {
    return `This action returns a #${id} board`;
  }

  update(id: number, updateBoardDto: UpdateBoardDto) {
    return `This action updates a #${id} board`;
  }

  remove(id: number) {
    return `This action removes a #${id} board`;
  }
}

NestJS(Nest)란?

https://docs.nestjs.com/

 

NestJs는 효율적이고, 확장 가능한 Node.js 서버 애플리케이션을 구축하기 위한 프레임워크이다.

프로그레시브 Javascript를 사용하고, Typescript로 구축되어 Typescript를 지원할 뿐만 아니라 Javascript도 지원한다.

 


1. Node.js 설치하기

https://nodejs.org/ko/download

 

NestJS는 Node.js 기반이기 때문에 위의 NodeJs 공식 사이트에서 자신에게 맞는 버전을 설치한다.

터미널에서 node -v 명령어를 실행하면 설치된 버전을 확인할 수 있다.

 

※ Node.js 버전은 14 이상을 설치해야 한다. ※

 

2. @nestjs/cli 설치하기

터미널에서 npm i -g @nestjs/cli 명령어를 실행한다.

Nest CLI는 Nest 애플리케이션을초기화하고, 개발 및 유지 보수하는데 도움이 되는 명령줄 인터페이스 도구이다.

 

3. nest 프로젝트 만들기

터미널에서 nest new project-name 명령어를 실행한다.

project-name 부분에 원하는 폴더명을 작성하면 된다.

 

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

터미널에서 npm run start 또는 npm run start:dev 명령어를 실행하고, Hello World! 화면이 나온다면 로컬호스트 연결에 성공한 것이다.

 

 

5. 로컬호스트 포트 변경하기

개발 작업의 편의성을 위해 포트를 변경했고, 꼭 필요한 것은 아니다.

 

  • src/main.ts

app.listen(3000) 부분을 3002로 변경했다.

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3002);
}

bootstrap();

06-1 반복기 이해하기

반복기와 반복기 제공자

1. next 라는 이름의 메서드를 제공한다.

2. next 메서드는 value, done 이라는 두 개의 속성을 가진 객체를 반환한다.

 

createRangeIterable 함수는 next 메서드를 return 해서 반복기 역할을 제공하고, 이를 '반복기 제공자(iterable)' 라고 한다.

사용자가 타입스크립트로 for...of 구문을 작성하면 TSC 컴파일러는 반복기 제공자와 반복기를 사용하는 코드로 바꿔준다.

export const createRangeIterable = (from: number, to: number) => {
  let currentValue = from

  return {
    next() {
      const value = currentValue < to ? currentValue++ : undefined
      const done = value == undefined

      return {value, done}
    }
  }
}

const iterator = createRangeIterable(1, 4) // 1부터 4까지 반복기, 함수 호출일 뿐 반복기는 동작하지 않음

while(true) {
  const {value, done} = iterator.next() // next 메서드를 호출하면서 반복기가 동작한다.
  if(done) break

  console.log(value) // 1 2 3
}

 

반복기는 왜 필요한가?

반복기 제공자는 어떤 범위의 값을 한 번에 생성해서 배열에 담지 않고 값이 필요할 때만 생성한다.

따라서 시스템 메모리의 효율성이라는 관점에서 보았을 때 메모리를 훨씬 적게 소모한다.

 

for...of 구문과 [Symbol.iterator] 메서드

위의 코드에서 createRangeIterable 함수를 for...of 구문으로 작성하면 아래 오류가 발생하고, 이 오류를 해결하기 위해서는 클래스로 구현해야 한다.

'Type '{ next(): { value: number; done: boolean; }; }' is not an array type or a string type or does not have a '[Symbol.iterator]()' method that returns an iterator.'

 

클래스 메서드느 자바스크립트의 function 키워드가 생략되었을 뿐 사실상 function 키워드로 만들어진 함수이다.

function 키워드로 만들어진 함수는 내부에서 this 키워드를 사용할 수 있고, next 함수 또한 function 키워드가 생략된 메서드이므로 컴파일러가 next의 this로 해석하지 않게 하는 자바스크립트의 유명한 코드 트릭이다.

export class RangeIterable {
  constructor(public from: number, public to: number) {}

  [Symbol.iterator]() {
    const that = this
    let currentValue = that.from

    return {
      next() {
        const value = currentValue < that.to ? currentValue++ : undefined
        const done = value == undefined

        return {value, done}
      }
    }
  }
}

const iterator = new RangeIterable(1, 4)

for(let value of iterator) {
  console.log(value) // 1 2 3
}

 

Iterable<T>와 Iterator<T> 인터페이스

타입스크립트는 반복기 제공자에 Iterable<T>와 Iterator<T> 제네릭 인터페이스를 사용할 수 있다.

 


06-2 생성기 이해하기

생성기는 function* 키워드로 만든 함수를 의미한다.

yield는 반드시 function* 키워드를 사용한 함수에서만 호출할 수 있고, yield는 return 키워드처럼 값을 반환한다.

export function* generator() {
  console.log('generator started.')

  let value = 1

  while(value < 4)
    yield value++

  console.log('generator fininsed.')
}

for(let value of generator())
  console.log(value)
  
// generator started.
// 1
// 2
// 3
// generator fininsed.

 

setInterval 함수와 생성기의 유사성

생성기가 동작하는 방식을 '세미코루틴' 이라고 한다. 

세미코루틴은 타입스크립트처럼 단일 스레드로 동작하는 프로그래밍 언어가 다중 스레드로 동작하는 것처럼 보이게 한다.

프로그램의 출력 내용만 보면 생성기 방식과 동일하지만, setInterval 함수가 동작하는 구조는 멀티 스레드가 동작하는 방식과 유사하다.

따라서 생성기는 일반적인 타입스크립트 코드가 다른 방식으로 동작하는 것을 기억해야 한다.

 

단일 스레드(싱글 스레드) vs 다중 스레드(멀티 스레드)

스레드란, 프로세스가 할당받은 자원을 시용하는 실행의 단위이다.

단일 스레드는 메인 스레드 하나만 가지고 작업을 처리하고, 작업을 순서대로 처리한다. (하나의 레지스터와 스택)

다중 스레드는 메인 스레드 외에 추가적인 스레드를 이용하여 병렬적으로 작업을 처리한다. context switching이 빠르게 일어나는 것이기 때문에 사용자 입장에서 프로그램들이 동시에 수행되는 것처럼 보인다.

 

let count = 0

console.log('program started.')

const id = setInterval(() => {
  if(count >= 3) {
    clearInterval(id)
    console.log('program finished.')
  } else {
    console.log(count++)
  }
}, 1000)

// program started.
// 0
// 1
// 2
// program finished.

 

function* 키워드

  1. function* 키워드로 함수를 선언한다.
  2. 함수 몸통 안에 yield문이 있다.

 

yield 키워드

  1. 반복기를 자동으로 만들어 준다.
  2. 반복기 제공자 역할도 수행한다.

 

function* 키워드로 선언된 함수만 생성기이고, 화살표 함수로는 생성기를 만들 수 없다.

function* getPage(pageSize: number = 1, list: number[]) {
  let output = []
  let index = 0

  while (index < list.length) {
    output = []

    for (let i = index; i < index + pageSize; i++) {
      if (list[i]) {
        output.push(list[i])
      }
    }

    yield output
    index += pageSize
  }
}

let page = getPage(3, [1, 2, 3, 4, 5, 6, 7, 8])

console.log(page.next()) // { value: [ 1, 2, 3 ], done: false }            
console.log(page.next()) // { value: [ 4, 5, 6 ], done: false }
console.log(page.next()) // { value: [ 7, 8 ], done: false }
console.log(page.next()) // { value: undefined, done: true }

 

 

yield* 키워드

yield는 단순히 값을 대상으로 동작하지만, yield*는 다른 생성기나 배열을 대상으로 동작한다.

function* gen12() {
  yield 1
  yield 2
}

function* gen12345() {
  yield* gen12()
  yield* [3, 4]
  yield 5
}

for(let value of gen12345()){
  console.log(value) // 1 2 3 4 5
}

 

yield 반환값

yield 연산자의 반환값은 반복기의 next 메서드 호출 때 매개변수에 전달하는 값이다.

function* gen(){
  let count = 3
  let select = 0

  while(count--){
    select = yield `select ${select}`
  }
}

const random = (max, min = 0) => Math.trunc(Math.random() * (max - min))

const iter = gen()

while(true){
  const {value, done} = iter.next(random(10, 1))

  if(done) {
    break
  } else {
    console.log(value)
  }
}

// select 0
// select 2
// select 5

 

 

 

참고자료

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

RESTful API란 무엇인가요?

RESTful API는 두 컴퓨터 시스템이 인터넷을 통한 정보를 안전하게 교환하기 위해 사용하는 인터페이스이다. 대부분의 애플리케이션들은 다양한 기능을 수행하기 위해 다른 내부 애플리케이션 및 서드 파티 애플리케이션과 통신해야 한다. 이런 통신 및 정보 교환을 위해 RESTful API를 사용하고, RESTful API는 안전하고 신뢰할 수 있으며 효율적인 소프트웨어 통신 표준을 따른다.

 

API란 무엇인가요?

애플리케이션 프로그래밍 인터페이스(Application Programming Interface, API)는 다른 소프트웨어 시스템과 통신하기 위해 따르는 규칙을 정의한다. 개발자는 다른 애플리케이션이 프로그래밍 방식으로 애플리케이션과 통신할 수 있도록 API를 생성한다. 웹 API는 클라이언트(사용자)와 웹 리소스(데이터, 정보) 사이의 게이트웨이라고 생각할 수 있다.

 

REST란 무엇인가요?

REST(Representational State Transfer)는 API 작동 방식에 대한 조건을 부과하는 소프트웨어 아키텍처이다. REST는 처음에 인터넷과 같은 복잡한 네트워크에서 통신을 관리하기 위한 지침으로 만들어졌다. 따라서 REST 기반 아키텍처를 사용하면 대규모의 고성능 통신을 안정적으로 지원할 수 있다. 또한 쉽게 구현하고 수정할 수 있고, 여러 플랫폼에서 사용할 수 있다.

 

  1. HTTP URL을 통해 자원(resource)을 명시하고,
  2. HTTP Method(GET, POST, PUT, DELETE 등)을 통해
  3. 해당 자원(URL)에 대한 CRUD(Create, Read, Update, Delete) Operation을 적용하는 것을 의미한다.

 

즉, REST는 웹에 존재하는 모든 자원(이미지, 동영상, DB정보 등)에 고유한 URL을 부여해 활용하는 방식이다.

 

REST 아키텍처 스타일에 따른 API를 REST API라고 하고,

REST 아키텍처를 구현하는 웹 서비스를 RESTful 웹 서비스라고 한다.

 

REST 구성

  • 자원(Resource) : URL
  • 행위(Verb) : HTTP Method
  • 표현(Representations)

 

REST 아키텍처 원칙

    1. 균일한 인터페이스 (uniform interface)

HTTP 표준에만 따른다면 웹 또는 모바일 플랫폼이나 언어, 기술에 상관없이 균일한 인터페이스로 사용할 수 있다.

URL로 지정한 리소스에 대해 조작이 가능하다.

 

    2. 무상태 (stateless)

작업을 위한 상태 정보를 따로 저장하고 관리하지 않는다. 세션이나 쿠키 정보를 저장하고 관리하지 않기 때문에 API 서버는 들어오는 요청만 처리한다.

 

    3. 캐시 가능성 (cacheable)

서버 응답 시간을 개선하기 위해 클라이언트 또는 중개자에 일부 응답을 저장하는 프로세스인 캐싱을 지원한다. RESTful 웹 서비스는 캐시 가능 또는 캐시 불가능으로 정의되는 API 응답을 사용하여 캐싱을 제어한다.

 

    4. 계층화 시스템 (layered system)

다층 계층으로 구성되어 있어 보안, 로드 밸런싱, 암호화 계층을 추가해 구조상 유연성을 둘 수 있고, 프록시, 게이트웨이 같은 네트워크 기반의 중간 매체를 사용할 수 있다.

클라이언트는 클라이언트와 서버 사이의 다른 승인된 중개자에게 연결할 수 있다. 서버는 요청을 다른 서버에 전달할 수 있다. 보안, 비즈니스 로직 등 여러 서버에서 실행될 수 있도록 웹 서비스를 설계할 수 있다.

 

    5. 클라이언트-서버 구조 (client-server)

클라이언트와 서버는 역할을 분리하여 상호 의존성을 줄였다. 클라이언트는 사용자 인증, 상태 관리와 서버 리소스 요청을 책임지고, 서버는 API, 비즈니스 로직, 데이터 저장을 책임진다.

 

RESTful API 장점

    1. 확장성

REST API는 무상태, 캐시 등의 기능을 통해 클라이언트-서버 상호 작용을 최적화하고, 통신 병목 현상을 일으키지 않으면서 확장성을 지원한다.

 

    2. 유연성

REST API는 클라이언트-서버 분리를 지원한다. 각 부분이 독립적으로 발전할 수 있도록 다양한 서버 구성 요소를 단순화하고 분리한다. 서버 애플리케이션을 변경해도 클라이언트 애플리케이션에 영향을 주지 않는다.

 

    3. 독립성

REST API는 클라이언트와 서버를 명확하게 분리한다. API 설계에 영향을 주지 않고 다양한 프로그래밍 언어로 클라이언트 및 서버 애플리케이션을 작성할 수 있고, 통신에 영향을 주지 않고 양쪽의 기본 기술을 변경할 수 있다.

 

RESTful API 작동 방식

  1. 클라이언트가 서버에 요청한다. 클라이언트가 API 문서에 따라 서버가 이해하는 방식으로 요청 형식을 지정한다.
  2. 서버가 클라이언트를 인증하고 해당 요청을 수행할 수 있는 권한이 있는지 확인한다.
  3. 서버가 요청을 수신하고 내부적으로 처리한다.
  4. 서버가 클라이언트에 응답(요청한 모든 정보)을 보낸다. 응답에는 요청이 성공했는지 여부도 포함된다.

 

RESTful API 인증 방법

  1. HTTP 인증: 기본 인증(사용자 이름과 암호), 전달자 인증(전달자 토큰)
  2. API 키
  3. OAuth: 암호와 토큰 결합

 

RESTful API 클라이언트 요청 포함 사항

  1. 고유 리소스 식별자: URL
  2. 메서드: GET, POST PUT, DELETE 등
  3. HTTP 헤더

 

HTTP 메서드

  • POST : 현재 리소스(Collection)보다 한 단계 아래의 리소스(Document) 생성
  • GET : 현재 리소스(Collection, Document)를 조회
  • PUT : 현재 리소스(Document)의 정보 수정 (해당 자원의 전체를 수정)
  • DELETE : 현재 리소스(Document)를 삭제
  • PATCH : 현재 리소스(Document)를 수정 (해당 자원의 일부를 수정)

 

RESTful API 서버 응답 포함 사항

  1. 상태 포시줄: 200, 201, 400, 404 등
  2. 메시지 본문: XML, JSON, RSS
  3. 헤더: 헤더 또는 메타데이터

 

응답 코드

  • 1xx : 전송 프로토콜 수준의 정보 교환
  • 2xx : 클라어인트 요청이 성공적으로 수행
  • 3xx : 클라이언트는 요청을 완료하기 위해 추가적인 행동을 취해야 함
  • 4xx : 클라이언트의 잘못된 요청
  • 5xx : 서버쪽 오류로 인한 상태코드

 

 

 

참고자료

https://aws.amazon.com/ko/what-is/restful-api/

https://meetup.nhncloud.com/posts/92

https://brainbackdoor.tistory.com/53

+ Recent posts