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! 타입스크립트 프로그래밍

목표

  • 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! 타입스크립트 프로그래밍

+ Recent posts