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

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

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

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

05-1 배열 이해하기

자바스크립트와 타입스크립트에서 배열은 다른 언어와 다르게 객체이다.

 

  • new Array 배열 선언하기

new Arrar 키워드를 통해 배열을 만들 수 있고, 배열에 담긴 값을 아이템(item) 또는 원소(element) 라고 한다.

let arr: number[] = new Array

arr.push(1)
arr.push(2)
arr.push(3)

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

 

  • [] 단축 구문 배열 선언하기

[] 단축 구문을 사용해서 배열을 만들 수 있다.

// [] 단축 구문
const numArr: number[] = [1, 2, 3]
const strArr: string[] = ['hello', 'world']

console.log(num) // [1, 2, 3]
console.log(str) // ['hello', 'world']

 

문자열과 배열 간 변환

타입스크립트에서는 문자 타입이 없고, 문자열의 내용 또한 변경할 수 없다.

따라서 문자열을 변경하기 위해서는 문자열을 배열로 변환해야 한다.

 

let str: string = 'hello'
let strArr: string[] = str.split('')

console.log(strArr.join('-')) // h-e-l-l-o

 

인덱스 연산자

인덱스 연산자, [인덱스]는 배열의 특정 위치에 있는 값을 알 수 있다.

let numbers: number[] = [1, 2, 3]

for(let i = 0; i < numbers.length; i++) {
    console.log(numbers[i])
}

// 1 2 3

 

배열의 비구조화 할당

객체뿐만 아니라 배열에도 비구조화 할당을 적용할 수 있다.

비구조화 할당이란, 배열이나 객체 속성을 해체하여 그 값을 변수에 담을 수 있게 하는 표현식이다.

let arr: number[] = [1, 2, 3, 4, 5]
let [first, second, third, ...rest] = arr

console.log(first, second, third, rest) // 1 2 3 [ 4, 5 ]

 

for ...in 문

배열의 인덱스 값을 순회한다.

let names: string[] = ['다연', '하영', '희재']

for(let index in names) {
    console.log(`[${index}]: ${names[index]}`)
}

// [0]: 다연 [1]: 하영 [2]: 희재

 

for ...of 문

배열의 아이템 값을 대상으로 순회한다. (인덱스값을 알 수 없다.)

let names: string[] = ['다연', '하영', '희재']

for(let index of names) {
    console.log(index)
}

// 다연 하영 희재

 

제네릭 방식 타입

배열을 다루는 함수를 작성할 때는 고정된 타입을 함수를 만들기보다는 T[] 형태(제네릭 타입)로 배열의 아이템 타입을 한꺼번에 표현하는 것이 편리하다.

타입스크립트는 타입 변수가 생략된 제네릭 함수를 만나면 타입 추론을 통해서 생략된 타입을 찾아낸다.

 

제네릭 사용 이유
  1. 한 가지 타입보다 여러 가지 타입에서 동작하는 컴포넌트를 생성할 수 있다.
  2. 재사용성이 높은 함수와 클래스를 생성할 수 있다.
  3. any 타입을 사용하는 대신에 사용하기 때문에 오류를 쉽게 검출할 수 있다.

 

function sort<T>(item: T[]): void {
    console.log(item.sort())
}
 
const nums: number[] = [3, 2, 5, 4, 1];
const chars: string[] = ['d', 'a', 'e', 'c', 'b'];
 
sort<number>(nums) // [ 1, 2, 3, 4, 5 ]
sort<string>(chars) // [ 'a', 'b', 'c', 'd', 'e' ]

 

전개 연산자

배열에서도 전개 연산자를 사용할 수 있다.

const arr1: number[] = [1, 2, 3]
const arr2: string[] = ['a', 'b', 'c']
const arr3: (number | string)[] = [...arr1, ...arr2]

console.log(arr3) // [ 1, 2, 3, 'a', 'b', 'c' ]

 

range 함수 구현

range 함수는 재귀 함수 스타일로 동작하고, 배열을 생성해 준다.

const range = (from: number, to: number): number[] => {
    return from < to ? [from, ...range(from + 1, to)] : []
}

let numbers: number[] = range(1, 5)

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

 


05-2 선언형 프로그래밍과 배열

명령형 프로그래밍

ex) for 문

  • 입력 데이터 얻기
  • 입력 데이터 가공해 출력 데이터 생성
  • 출력 데이터 출력

 

선언형 프로그래밍

ex) for 문을 사용하지 않고 모든 데이터를 배열에 담는 형식

  • 문제를 푸는 데 필요한 모든 데이터 배열에 저장
  • 입력 데이터 배열을 가공해 출력 데이터 배열 생성
  • 출력 데이터 배열에 담긴 아이템 출력

 

1부터 100까지 더하기 

  • 명령형 프로그래밍
let sum = 0

for(let i = 1; i <= 100; i++) {
    sum = sum + i
}

console.log(sum) // 5050

 

  • 선언형 프로그래밍
const range = (from: number, to: number): number[] => {
    return from < to ? [from, ...range(from + 1, to)] : []
}

const numbers: number[] = range(1, 101)

console.log(numbers.reduce((acc, cur) => acc + cur)) // 5050

 


05-3 배열의 map, reduce, filter 메서드

filter 메서드

깊은 복사의 형태로 동작한다. (= 원본 배열을 변경하지 않는다.)

const arr: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

const odd: number[] = arr.filter((item, index) => item % 2 !== 0)
const even: number[] = arr.filter((item, index) => item % 2 === 0)

console.log(odd) // [ 1, 3, 5, 7, 9 ]
console.log(even) // [ 2, 4, 6, 8, 10 ]

 

map 메서드

깊은 복사의 형태로 동작한다. (= 원본 배열을 변경하지 않는다.)

const arr: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

const double = arr.map((item, index) => item * 2)

console.log(double) // [2,  4,  6,  8, 10, 12, 14, 16, 18, 20]

 

reduce 메서드

const arr: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

const total = arr.reduce((acc, cur) => acc + cur)

console.log(total) // 55

 


05-4 순수 함수와 배열

함수형 프로그래밍에서 함수는 '순수 함수'라는 조건을 만족해야 한다.

그러나 타입스크립트의 배열에는 순수 함수 조건에 부합하지 않는 메서드들이 많다.

 

순수 함수란?

'순수 함수'는 부수 효과(side-effect)가 없는 함수를 의미한다. (부수 효과가 있으면 '불순 함수' 라고 한다.)

 

  • 함수 몸통에 입출력에 관련된 코드가 없어야 한다.
  • 함수 몸통에서 매개변숫값을 변경시키지 않는다. (즉, 매개변수는 const나 readonly 형태로만 사용한다.)
  • 함수는 몸통에서 만들어진 결과를 즉시 반환한다.
  • 함수 내부에 전역 변수나 정적 변수를 사용하지 않는다.
  • 함수가 예외를 발생시키지 않는다.
  • 함수가 콜백 함수로 구현되었거나 함수 몸통에 콜백 함수를 사용하는 코드가 없다.
  • 함수 몸통에 Promise와 같은 비동기 방식으로 작동하는 코드가 없다.

 

  • 순수 함수
function pure(a: number, b: number): number {
    return a + b
}

console.log(pure(1, 2)) // 3

 

  • 불순 함수

매개변수 arr이 변경된다.

function impure(arr: number[]): void {
    console.log(arr) // []
    
    arr.push(100)
    
    console.log(arr) // [100]
}

impure([])

 

타입 수정자 readonly

타입스크립트는 순수 함수 구현을 쉽게 하도록 readonly 키워드를 제공한다.

또는 인터페이스, 클래스, 함수의 매개변수는 let, const 키워드 없이 선언하기 때문에 심벌에 const와 같은 효과를 주기 위해서 readonly 타입 수정자가 필요하다.

function impure(arr: readonly number[]): void {
    // Property 'push' does not exist on type 'readonly number[]'.
    arr.push(100)
}

 

불변과 가변

  • 불변 변수

const 또는 readonly 키워드로 선언된 변수이다.

초기값을 항상 유지한다.

 

  • 가변 변수

var 또는 let 키워드로 선언된 변수이다.

언제든 변수의 값을 변경할 수 있다.

 

깊은 복사와 얕은 복사

순수 함수를 구현할 때는 매개변수가 불변성을 유지해야 한다. 매개변수를 가공하려고 할 때 깊은 복사를 실행해 매개변숫값이 변경되지 않게 해야 한다.

그러나 새로운 변수에 기존 변수를 할당하는 방식으로 복사를 시도하면 객체와 배열은 얕은 복사 방식으로 동작하기 때문에 주의해야 한다.

 

  • 깊은 복사

원본 변수의 값이 변경되지 않는다.

let original = 1
let copy = original

copy = copy + 2

console.log(original) // 1
console.log(copy) // 3

 

  • 얕은 복사

원본 배열의 값이 변경된다.

let original = [1, 2, 3]
let copy = original

cop.push(4)

console.log(original) // [1, 2, 3, 4]
console.log(copy) // [1, 2, 3, 4]

 

전개 연산자와 깊은 복사

전개 연산자를 사용해 객체와 배열을 깊은 복사할 수 있다.

let originArr = [1, 2, 3]
let copyArr = [...originArr]

copyArr.push(4)

console.log(originArr) // [ 1, 2, 3 ]
console.log(copyArr) // [ 1, 2, 3, 4 ]

 

배열의 sort 메서드를 순수 함수로 구현하기

sort 메서드는 원본 배열의 내용을 변경한다. 

따라서 원본 배열을 유지한채 sort 메서드를 사용하고 싶다면 전개 연산자의 깊은 복사 기능을 먼저 사용하면 된다.

 

  • sort 얕은 복사

원본 배열의 값이 변경된다.

let arr: number[] = [3, 5, 2, 1, 4]
let sortArr: number[] = arr.sort()

console.log(arr) // [ 1, 2, 3, 4, 5 ]
console.log(sortArr) // [ 1, 2, 3, 4, 5 ]

 

  • sort 깊은 복사

원본 배열의 값이 변경되지 않는다.

let arr: number[] = [3, 5, 2, 1, 4]
let sortArr: number[] = [...arr].sort()

console.log(arr) // [ 3, 5, 2, 1, 4 ]
console.log(sortArr) // [ 1, 2, 3, 4, 5 ]

 

배열의 filter 메서드와 순수한 삭제

보통 배열에서 특정 아이템을 삭제할 때 splice 메서드를 많이 사용하는데 splice 메서드는 원본 배열을 변경하므로 순수 함수에서는 사용할 수 없다.

filter 메서드는 깊은 복사 형태로 동작하기 때문에 원본 배열을 유지한 채 특정 아이템을 삭제할 수 있다.

interface Data {
  id: number
  title: string
}

const data: Data[] = [
  { id: 1, title: '제목1' },
  { id: 2, title: '제목2' },
  { id: 3, title: '제목3' }
]

let filterData = data.filter((item, index) => item.id !== 1)

console.log(data) // [ { id: 1, title: '제목1' }, { id: 2, title: '제목2' }, { id: 3, title: '제목3' } ]
console.log(filterData) // [ { id: 2, title: '제목2' }, { id: 3, title: '제목3' } ]

 

가변 인수 함수와 순수 함수

가변 인수(variadic arguments)는 함수를 호출할 때 전달하는 인수의 개수를 제한하지 않는다.

 

  1. 가변 인수로 호출할 때 타입에 상관없이 동작하게 하기 위해서 제네릭 타입으로 구현한다.
  2. 전개 연산자를 사용해 가변 인수를 표현한다.
  3. 가변 인수로 전달하는 값이 배열이기 때문에 타입을 배열의 배열로 선언하고, 매개변수는 변경되면 안되기 때문에 readonly로 선언한다.

 

const mergeArr = <T>(...arr: readonly T[][]): T[] => {
  let result: T[] = []

  for(let i = 0; i < arr.length; i++) {
    const array: T[] = arr[i]
    result = [...result, ...array]
  }

  return result
}

console.log(mergeArr([1, 2, 3])) // [1, 2, 3]
console.log(mergeArr(['hello', 'world'])) // [ 'hello', 'world' ]
console.log(mergeArr([{name: '희재', age: 27}])) // [ { name: '희재', age: 27 } ]

 

순수 함수를 고려하면 사실상 자바스크립트 배열이 제공하는 많은 메서드를 사용할 수 없지만,
전개 연산자 등의 메커니즘을 사용하면 순수 함수 형태로 간단하게 표현될 수 있음을 알아야 한다.

 


05-5 튜플 이해하기

튜플이란, 길이와 타입이 고정된 배열이다. 배열에 담긴 데이터를 만들 때 순서를 무시하고 만들게 되는 오류를 방지할 수 있다.

또한 튜플은 물리적으로 배열이기 때문에 배열처럼 인덱스 연산자나 비구조화 할당문을 적용할 수 있다.

// 타입 별칭으로 튜플의 의미를 정확하게 한다.
type tupleType = [number, string]

const tuple1: tupleType = [1, 'title1']
const tuple2: tupleType = ['title2', 2] // Type 'string' is not assignable to type 'number'.

console.log(tuple1[0], tuple1[1]) // 1, title1 (인덱스 연산자 사용)

// 비구조화 할당
let [id, title] = tuple1 
console.log(id, title) // 1, title1

 

그러나 튜플의 의미와 맞지 않게 push 메서드를 사용해 배열의 길이를 늘릴 수 있어 주의해야 한다. 

type tupleType = [number, string]

const tuple: tupleType = [1, 'title']

tuple.push('content')
tuple.push(true) // Argument of type 'boolean' is not assignable to parameter of type 'string | number'.

console.log(tuple) // [ 1, 'title', 'content' ]

 

 

 

참고자료

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

04-1 함수 선언문

자바스크립트에서 함수는 function 키워드로 만든 함수=> 기호로 만든 화살표 함수 2가지가 있다.

 

타입스크립트 함수 선언문은 매개변수와 함수 반환값(return)에 타입 주석을 붙이는 형태이다.

함수 선언문에서도 매개변수와 반환값에 타입 주석을 생략할 수 있지만, 타입이 생략되어 있으면 함수의 구현 의도를 알기 어렵기 때문에 바람직하지 않다.

 

function addNumber(~~)의 괄호 안의 값을 parameter, 매개변수 라고 하고,

콘솔의 addNumber(~~)의 괄호 안의 값을 argument, 인수, 인자 라고 한다.

 

  • function 키워드로 만든 함수
function addNumber(num1: number, num2: number): number {
  return num1 + num2
}

console.log(addNumber(1, 2)) // 3

 

  • => 기호로 만든 화살표 함수
const addString = (str1: string, str2: string): string => str1 + str2

console.log(addString('가나다', '라마바')) // 가나다라마바

 

void 타입

void 타입은 값을 반환하지 않는 함수이다.

function printMe(name: string, age: number): void {
  console.log(`name: ${name}, age: ${age}`)
}

printMe('HeeJae', 29) // name: HeeJae, age: 29

 

type 키워드로 타입 별칭 만들기

타입 별칭(type alias)의 type 키워드는 기존에 존재하는 타입을 단순히 이름만 바꿔서 사용할 수 있게 해 준다.

함수를 선언할 때, 타입 별칭을 사용하면 매개변수의 개수, 타입, 반환 타입이 다른 함수를 선언하는 것을 미리 방지할 수 있다.

type stringNumberFunc = (arg1: string, arg2: number) => void

let f: stringNumberFunc = function(a: string, b: number): void {
  console.log(a, b)
}

f('Heejae', 29) // { name: 'Heejae', age: 29 }
f('Heejae') // Expected 2 arguments, but got 1.
f() // Expected 2 arguments, but got 0.

 

type vs interface

타입 별칭과 인터페이스의 가장 큰 차이점은 확장 가능, 불가능의 여부이다.

인터페이스는 확장이 가능하지만, 타입별칭은 확장이 불가능하기 때문에 타입 별칭보다는 인터페이스를 사용하는 것을 추천한다.

 

undefined 관련 주의 사항

타입스크립트에서 undefined은 타입이면서 값이기 때문에 값을 변경할 수가 없다.

초기화를 안 했거나 값이 없을 경우, undefined으로 나오는데 오류를 방지하기 위해서 매개변수 값이 undefined인지 판별하는 코드를 작성해야 한다.

 

필수 속성은 값이 있어야 하기 때문에 undefined를 할당하면 오류가 발생하고,

선택속 성은 값이 없어도 되기 때문에 undefined를 판별하는 코드가 있으면 오류가 발생하지 않는다.

 

FIXME: getName(undefined!)에서 !가 없으면 오류, Argument of type 'undefined' is not assignable to parameter of type 'INameable'.

// 필수속성 undefined
interface INameable {
  name: string
}

function getName(userName: INameable) {
  return userName !== undefined ? userName.name : 'unknown name'
}

console.log(getName(undefined!)) // unknown name
console.log(getName({name: 'Heejae'})) // Heejae
// 선택속성 undefined
interface IAgeable {
  age?: number
}

function getAge(userAge: IAgeable) {
  return userAge !== undefined && userAge.age ? userAge.age : 'unknow age'
}

console.log(getAge(undefined!)) // unknow age
console.log(getAge({age: 29})) // 29

 

선택적 매개변수

인터페이스의 선택 속성처럼 함수의 매개변수에도 물음표(?)를 붙여 선택적 매개변수를 만들 수 있다.

function userList(name: string, age?: number) {
  console.log(`name: ${name}, age: ${age}`)
}

userList('Heejae', 29) // name: Heejae, age: 29
userList('Heejae') // name: Heejae, age: undefined

 


04-2 함수 표현식

타입스크립트와 자바스크립트는 객체지향 언어와 함수형 언어의 특징을 모두 가지고 있다.

 

함수 표현식을 선언할 때는 let보다는 const 키워드로 선언하는 것이 바람직하다.

let 변수는 값이 변할 수 있고, const 변수는 값이 절대로 변하지 않기 때문이다. 

 

  • 변수 선언문 형태의 함수
const add = new Function('a', 'b', 'return a + b')

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

 

  • 함수 표현식 형태의 함수
const add = function(a: number, b: number) { return a + b }

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

 

표현식: 리터럴, 연산자, 변수, 함수 등 복합적으로 구성된 코드 형태

함수 표현식(=익명함수): 함수 이름을 제외한 함수 코드

함수 호출: 함수 표현식의 몸통 부분을 실행

 

일등 함수

일등 함수란, 변수와 함수를 구분하지 않는다는 의미이다.

위의 코드를 보면 add 라는 변수에 함수를 할당했기 때문에 add가 변수인지 함수인지 사실상 구분할 수 없다.

 


04-3 화살표 함수와 표현식 문

C언어는 모든 문장이 반드시 세미콜론 ;으로 끝나야 하고, C언어 구문을 참조해 만든 ES5 자바스크립트 또한 모든 문장 끝에 세미콜론이 있어야 한다. 반면에 ESNext 자바스크립트와 타입스크립트에서는 세미콜론을 생략할 수 있다.

 

  • 실행문 지향 언어

실생문은 CPU에서 실행되는 코드를 의미한다. 

실행문은 CPU에서 실행만 될 뿐 결과는 알려주지 않기 때문에 return 키워드를 사용해서 실행된 결과를 알려줘야 한다.

 

ex) C언어, ES5, ESNext, 타입스크립트

const add = (a: number, b: number): number => {return a + b}

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

 

  • 표현식 지향 언어

표현식은 CPU에서 실행된 결과를 return 키워드를 사용하지 않아도 알려준다.

 

ex) 스칼라, ESNext, 타입스크립트

const add = (a: number, b: number): number => a + b

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

 

실행문과 표현식을 동시에 지원하는 언어 = 다중 패러다임 언어= ESNext, 타입스크립트

 

복합 실행문과 return

복합 실행문은 중괄호{}를 사용해서 작성하고, 컴파일러는 여러 줄의 실행문을 한 줄의 실행문으로 인식한다.

실행문에서 return을 작성하지 않으면 'void 함수 반환 값이 사용되었습니다' 라는 오류가 발생한다.

return 키워드는 반드시 함수의 몸통에서만 사용할 수 있다.

 

복합 실행문의 유효범위는 local scope(지역 범위)이다.

function f() {
  let x = 1
  return x
}

function g() {
  let x = 2
  return x
}

console.log(f()) // 1
console.log(g()) // 2

 


04-4 일등 함수 살펴보기

콜백 함수

콜백 함수는 매개변수 형태로 동작하는 함수이다.

콜백 함수는 프레임워크의 API 구현에 매우 유용하다.

export const init = (callback: () => void): void => {
  console.log('first message')
  callback()
  console.log('second message')
}

init(() => console.log('function message'))

// first message
// function message
// second message

 

중첩 함수

함수형 언어에서 함수는 함수 표현식 안에 또 다른 함수를 작성할 수 있다.

const calc = (num1: number, num2: (result: number) => void): void => {
  let add = (a: number, b: number) => a + b

  function multiply(a: number, b: number) {return a * b}
  let result = multiply(add(1, 2), num1)

  num2(result)
}

calc(30, (result: number) => console.log(`result is ${result}`)) // result is 90

 

고차 함수와 클로저, 그리고 부분 함수

고차 함수는 또 다른 함수를 반환하는 함수이다. 함수형 언어에서 함수 표현식 또한 값이기 때문에 다른 함수를 반환할 수 있다.

고차 함수는 함수 안에 또 다른 함수가 있기 때문에 변수의 스코프가 중요한데, 함수를 return 하고, return 하는 함수가 클로서를 형성한다.

const add = (a: number): (b: number) => number => (b: number): number => a + b
const result = add(10)(20)

console.log(result) // 30

 


04-5 함수 구현 기법

매개변수 기본값 지정하기

선택적 매개변수는 항상 그 값이 undefined로 고정된다. 만약 함수 호출 시, 인수를 전달하지 않더라도 매개변수에 어떤 값을 설정하고 싶다면 기본값을 지정할 수 있고, 이를 디폴트 매개변수라고 한다.

export type Person = {name: string, age: number}

export const makePerson = (name: string, age: number = 10): Person => {
    // 단축 구문(shorthand)
    const person = {name, age}
    return person
}

console.log(makePerson('Jack')) // { name: 'Jack', age: 10 }
console.log(makePerson('Jack', 33)) // { name: 'Jack', age: 33 }

 

객체를 반환하는 화살표 함수 만들기

화살표 함수에서 객체를 반환할 때, 아래처럼 중괄호로 구현한다면 객체가 아닌 복합 실행문으로 해석하여 오류가 발생한다.

export type Person = {name: string, age: number}

// Left side of comma operator is unused and has no side effects.
export const makePerson = (name: string, age: number = 10): Person => {name, age}

소괄호로 감싸주어 객체임을 알려준다.

export type Person = {name: string, age: number}

export const makePerson = (name: string, age: number = 10): Person => ({name, age})

console.log(makePerson('Jack')) // { name: 'Jack', age: 10 }
console.log(makePerson('Jack', 33)) // { name: 'Jack', age: 33 }

 

매개변수에 비구조화 할당문 사용하기

비구조화: 구조화된 데이터의 일부만 사용하는 것

객체뿐만 아니라 함수의 매개변수도 비구조화 할당문을 적용할 수 있다.

export type Person = {name: string, age: number}

export const makePerson = ({name, age}: Person): void => {
    console.log(`My name is ${name}. My age is ${age}.`)
}

console.log(makePerson({name: 'Jack', age: 10})) // My name is Jack. My age is 10.

 

색인 키와 값으로 객체 만들기

색인 가능 타입(indexable type)은 {[key]: value} 형태를 의미하고, key와 value의 타입을 명시한다.

객체의 속성 이름을 변수로 만들 때 사용한다. (키 값을 변경할 수 있다.)

export type KeyValeuType = {
    [key: string]: string
}

const makeObject  = (key: string, value: string): KeyValeuType => (
    {[key]: value}
)

console.log(makeObject('key', 'value')) // { key: 'value' }
console.log(makeObject('NAME', 'Jack')) // { NAME: 'Jack' }

 


04-6 클래스 메서드

function 함수와 this 키워드

function 키워드로 만든 함수는 this를 사용할 수 있지만,

화살표 함수는 this를 사용할 수 없다.

 

클래스 메서드 구문

타입스크립트에서 메서드는 function으로 만든 함수 표현식을 담고 있는 속성이다.

생성자를 통해 전달된 값이 value에 설정되고, method가 호출돼서 값이 출력된다.

export class B {
    constructor(public value: number = 1) {}
    method(): void {
        console.log(`value: ${this.value}`)
    }
}

let a: B = new B()
a.method() // value: 1

let b: B = new B(2)
b.method() // value: 2

 

정적 메서드

메서드 이름 앞에 static을 붙여 정적 메서드를 만들 수 있다.

export class C {
    static whoAreYou(): string {
        return 'I am class C'
    }
}

export class D {
    static whoAreYou(): string {
        return 'I am class D'
    }
}

console.log(C.whoAreYou()) // I am class C
console.log(D.whoAreYou()) // I am class D

 

메서드 체인

메서드 체인(method chain)은 객체의 메서드를 이어서 계속 호출하는 방식의 코드를 의미하고, 타입스크립트로 메서드 체인을 구현하려면 메서드가 항상 this를 반환해야 한다.

export class Caculator {
    constructor(public value: number = 0) {}

    add(value: number) {
        this.value += value
        return this
    }

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

let calc = new Caculator
let result = calc.add(1).add(2).multiply(3).multiply(4).value

console.log(result) // (((0 + 1 + 2) * 3) * 4) = 36

 

 

 

참고자료

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

03-1 타입스크립트 변수 선언문

타입스크립트 타입

let name:string = '이희재'

let age:number = 29

let isAdult:boolean = true

let a:null = null

let b:undefined = undefined // undefined은 타입이면서 값이고, undefined 값만 가질 수 있다.

// 배열
let arr1:number[] = [1, 2, 3]
let arr2:Array<number> = [1, 2, 3]

let arr3:string[] = ['hello', 'world']
let arr4:Array<string> = ['hello', 'world']

// 튜플
let tuple:[string, number]

tuple = ['hello', 100]
tuple = [10, 100] // Type 'number' is not assignable to type 'string'.

tuple[0].toLowerCase()
tuple[1].toLowerCase() // Property 'toLowerCase' does not exist on type 'number'.

// void: 함수에서 아무것도 반환하지 않을 때 사용
function sayHello():void {
  console.log('hello')
}

// never: error를 반환하거나 영원히 끝나지 않는 함수일 때 사용
function infLoop():never {
  throw new Error()

  while(true) {
	
  }
}

// enum: 자바스크립트에는 없는 타입, 아무것도 정의하지 않으면 0부터 1씩 증가하면서 할당
enum Os1 {
  Window, // 0
  Ios,    // 1
  Android // 2
}

enum Os2 {
  Window = 5,
  Ios,    // 6
  Android // 7
}

// 숫자가 아닐 때는 단방향 맵핑만 된다.
enum Os3 {
  Window = 'win',
  Ios = 'ios',
  Android = 'and'
}

console.log(Os1) // Window: 0, Ios: 1, Android: 2
console.log(Os2[5]) // Window
console.log(Os3.Window) // win

 

any 타입

any 타입은 변수의 값이 타입과 무관하게 어떤 종류의 값도 저장할 수 있다.

let random:any = '랜덤'

random = 100
random = true
random = null
random = undefined
random = [1, 2, 3]

 

undefined 타입

undefined 타입은 타입이면서 값이고, 오직 undefined 값만 가질 수 있다.

변수를 선언하고, 초기화를 하지 않으면 undefined 값을 가지는 것이 아니라 에러가 발생하기 때문에 주의해야 한다.

초기값이 없을 경우, undefined으로 선언하고 null safe(?)를 사용하는 방식으로 해결할 수도 있지만, 변수 초기화를 해주는 것이 좋다.

let num:number
console.log(num) // Variable 'num' is used before being assigned.

let num:undefined
console.log(num) // undefined

 

let과 const 키워드

  • var

var 변수는 재선언(중복 선언)되고, 재할당(업데이트) 된다.

var 변수로 선언할 경우, 유지보수의 어려움이 있어 ESNext에서 var 키워드는 사용하지 말라고 권고한다.

 

  • let

let 변수는 재할당은 가능하지만, 재선언은 불가능하다.

let으로 선언한 변수는 코드에서 그 값이 수시로 변경될 수 있음을 암시한다.

 

  • const

const 변수는 재선언, 재할당 모두 불가능하다.

const로 변수를 선언할 때는 반드시 초깃값을 명시해야 하고, 변숫값이 절대로 바뀌지 않는다.

 

타입 주석

타입 주석(type annotation)은 자바스크립트 변수 선언문을 확장해 타입을 명시하는 것을 의미한다.

let으로 선언한 변수는 값을 변경할 수 있지만, 선언된 타입과 다른 타입의 값으로 바꾸려고 하면 오류가 발생한다.

let 변수 이름: 타입
const 변수 이름: 타입 = 초깃값

let name:string = '이희재'
let age:number = 29
let isAudult:boolean

age =  30
age = '삼십' // Type 'string' is not assignable to type 'number'.

 

타입 추론

타입스크립트는 자바스크립트와 호환성을 위해 타입 주석 부분을 생략할 수 있다.

타입 추론(type inference)은 대입 연산자(=)가 오른쪽 값에 따라 변수의 타입을 지정하는 것을 의미한다.

let name = '이희재' // name을 string로 판단
let age = 29 // age를 number로 판단

 

템플릿 문자열

템플릿 문자열은 변수에 담긴 값을 조합해 문자열을 만들 수 있게 한다.

`${변수 이름}`

let count = 10
let message = 'Your count'
let result = `${message} is ${count}`

console.log(result) // Your count is 10

 


03-2 객체와 인터페이스

object 타입은 인터페이스와 클래스의 상위 타입이다.

object 타입으로 선언된 변수는 속성 이름이 다른 객체를 자유롭게 담을 수 있다.

 

아래 코드는 오류가 나지 않는 정상 코드이다.

속성값이 달라져도 오류가 발생하지 않기 때문에 오류 검출 및 유지보수를 위해서 타입 스크립트의 인터페이스 구문이 생겨났다.

let obj:object = { name: '이희재', age: 29 }
obj = { first: 1, second: 2 }

 

인터페이스 선언문

interface 키워드는 객체의 타입을 정의할 수 있다.

인터페이스를 설계할 때 필수 속성과 선택 속성이 있고, 속성 이름 뒤에 물음표 기호를 붙이면 선택 속성이 된다.

// 속성들이 여러 개일 경우, 쉼표, 세미콜론 또는 줄바꿈을 구분자로 사용한다.
interface IPerson {
  name: string
  age: number
  isAdult?: boolean // 선택 속성
  readonly birthYear: number // 읽기만 가능하고, 값을 변경할 수 없다.
}


let person1:IPerson = {
  name: '이희재',
  age: 29,
  isAdult: true,
  birtyYear: 1995
}
person1.birthYear = 2000 // Cannot assign to 'birthYear' because it is a read-only property.


Type '{ name: string; }' is missing the following properties from type 'IPerson': age, birthYear
let person2:IPerson = {
  name: '이희재'
}


// 'number' does not exist in type 'IPerson'.
let person3:IPerson = {
  name: '이희재',
  age: 29,
  number: '010-1111-1111'
}

 

익명 인터페이스

익명 인터페이스는 interface 키워드도 사용하지 않고, 인터페이스 이름도 없는 인터페이스를 의미한다.

let ai: {
  name: string
  age: number
  etc?: boolean
} = { name: 'Tom', age: 20 }

console.log(ai) // { name: 'Tom', age: 20 }

 


03-3 객체와 클래스

클래스 선언문

타입스크립트는 C++나 자바 같은 객체지향 언어에서 흔히 볼 수 있는 class, private, public, protected, implements, extend와 같은 키워드를 제공한다.

 

타입스크립트에서 생성자 변수의 타입을 선언하지 않으면 에러가 발생한다.

class Person {
  name: string
  age?: number

  // 생성자: 클래스의 속성(name, age)를 선언할 수 있다.
  constructor(name: string, age?: number) {
    this.name = name
    this.age = age
  }
}

let jack:Person = new Person()
jack.name = 'Jack'
jack.age = 32

console.log(jack) // Person { name: 'Jack', age: 32 }

 

접근 제한자

  • public

클래스 내부, 자식 클래스, 클래스 인스턴스 모두 접근이 가능하다. 아무것도 작성하지 않으면 public으로 간주한다.

 

  • protected

클래스 내부, 자식 클래스에서 접근이 가능하다.

 

  • private

클래스 내부에서만 접근이 가능하다.  캡슐화

외부에서 필요하지 않은 메서드(ex. 비밀번호 암호화 로직) 또는 임의로 변경하면 안 되는 데이터(ex. 계산기에서의 값은 계산을 통해서만 변경, 직접 값을 변경 X)를 만들 때 주로 private로 선언한다.

 

 접근 가능성  public  protected  private
 클래스 내부  O  O   O
 자식 클래스 내부  O  O  X
 클래스 인스턴스  O  X  X

 

class Car {
  public color: string // Car { color: 'black' }
  protected color: string // Property 'color' is protected and only accessible within class 'Car' and its subclasses.
  private color: string // Property 'color' is private and only accessible within class 'Car'.
  
  constructor(color: string) {
    this.color = color
  }
}

let bmw:Car = new Car()
bmw.color = 'black'

console.log(bmw)

 

인터페이스 구현

implements 키워드는 클래스가 인터페이스를 구현할 때 사용한다.

클래스에는 반드시 인터페이스가 정의하고 있는 속성을 멤버 속성으로 포함해야 한다.

 

인터페이스가 정의하고 있는 속성을 모두 포함하고 있지 않으면(name 또는 age가 하나라도 없으면) 해당 에러가 발생한다.

Class 'User' incorrectly implements interface 'IUser'.   Property 'age' is missing in type 'User' but required in type 'IUser'.

interface IUser {
  name: string
  age: number
}

class User implements IUser {
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}

let tom:IUser = new User('Tom')

console.log(tom) // User { name: 'Tom', age: undefined }

 

추상 클래스

abstract 키워드는 추상 클래스를 정의할 때 사용하고, 직접 인스턴스를 생성할 수 없고, 상속만을 위해 사용된다.

즉, 프로퍼티나 메서드의 이름만 선언해 주고(몸체가 구현되지 않았다.), 구체적인 기능은 상속받은 쪽에서 작성할 때 사용한다.

 

클래스의 상속

extends 키워드를 사용해서 상속 클래스를 만든다.

super 키워드는 부모 클래스의 생성자를 호출할 수 있다.

abstract class Car {
  color: string
  wheel: number

  constructor(color: string, wheel: number) {
    this.color = color
    this.wheel = wheel
  }

  abstract changeColor(color: string): string
  abstract wheelCount(wheel: number): number
}

class kia extends Car {
  constructor(color: string, wheel: number) {
    super(color, wheel)
  }

  changeColor(): string {
    return 'change color'
  }

  wheelCount(): number {
    return 4
  }
}

const k5: kia = new kia('white', 4)

console.log(k5) // kia { color: 'white', wheel: 4 }
console.log(k5.changeColor()) // change color
console.log(k5.wheelCount()) // 4

 

static 속성

static 키워드는 다른 객체지향 언어처럼 클래스 정적 메서드를 정의한다. (정적인 속성을 가질 수 있다.)

정적 메서드는 인스턴스가 아닌 클래스 이름으로 호출하고, 클래스의 인터페이스를 생성하지 않아도 호출할 수 있다.

정적 메소드는 this를 사용할 수 없다.

class A {
  static first: number = 1
  second: number = 2
}

let first = A.first
let second = A.second // Property 'second' does not exist on type 'typeof A'.

console.log(first) // 1

 


03-4 객체의 비구조화 할당문

구조화

구조화(structuring)는 인터페이스나 클래스를 사용해 관련된 정보를 묶어 새로운 타입으로 표현하는 것을 의미한다..

// 구조화
export interface IPerson {
  name: string
  age: number
}

export interface ICompany {
  name: string
  age: number
}

let jack:IPerson = {
  name: 'Jack',
  age: 29
}

let naver:ICompany = {
  name: 'Naver',
  age: 10
}

console.log(jack) // { name: 'Jack', age: 29 }
console.log(naver) // { name: 'Naver', age: 10 }

 

비구조화

구조화된 데이터는 어떤 시점에서 일부만 사용해야 할 때가 있다.

이때 구조화된 데이터를 분해하는 것을 비구조화(destructuring) 라고 한다.

// 비구조화
let jack_name = jack.name
let jack_age = jack.age

console.log(jack_name, jack_age) // Jack 29

// 비구조화 할당: 중괄호로 묶어 각각의 초깃값으로 할당받는다.
let {name, age} = naver

console.log(name, age) // Naver 10

 

잔여 연산자

... 잔여 연산자를 사용하면 country와 city를 제외한 나머지 속성들이 담긴다.

let address:any = {
  country: 'Korea',
  city: 'Seoul',
  address1: '양천구',
  address2: '목동서로',
  address3: '221'
}

const {country, city, ...detail} = address

console.log(country) // 대한민국
console.log(city) // 서울
console.log(detail) // { address1: '양천구', address2: '목동서로', address3: '221' }

 

전개 연산자

... 전개 연산자를 사용하면 객체의 속성을 모두 전개해 새로운 객체로 만들어 준다.

let name = {name: 'Jack'}
let age = {age: 30}

const userInfo = {...name, ...age}

console.log(userInfo) // { name: 'Jack', age: 30 }

 


03-5 객체의 타입 변환

타입 변환(type conversion)은 특정 타입의 변숫값을 다른 타입의 값으로 변환할 수 있는 기능이다.

let person: object = {name: 'Jack', age: 32};

person.name // Property 'name' does not exist on type 'object'.

(<{name: string}>person).name // Jack

 

타입 단언

타입 단언(type assertion)은 컴파일러에게 타입을 확실히 알려주기 위해 사용하고,

강제로 타입을 지정하는 것이기 때문에 타입을 만족하지 않더라도 오류를 무시한다.

 

타입을 변경하기 위해서 as 타입 단언을 사용하지 않는 것은 좋지 않지만, 원시 타입에서 원시 타입으로 변경하거나 response(then, catch)에서 받은 값의 데이터 타입을 모를 때, as 타입 단언을 통해 데이터 타입을 주입시키는 용도로도 사용할 수 있다.

 

콜론(:)을 사용해서 타입을 지정하는 것은 타입 선언이고, 타입 선언을 이용하면 할당되는 값이 선언된 타입을 만족하는지 검사한다.

주의: react의 JSX에서 <> 꺽쇠 괄호를 사용하기 때문에 리액트에서는 as를 사용한다.

(<타입>객체)
(객체 as 타입)

export default interface INameable {
  name: string
}

let obj:object = {name: 'Jack'}

let name1 = (<INameable>obj).name
let name2 = (obj as INameable).name

console.log(typeof obj, obj) // object { name: 'Jack' }
console.log(typeof name1, name1) // string Jack
console.log(typeof name2, name2) // string Jack

 

 

참고자료

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

02-1 타입스크립트 프로젝트 만들기

타입스크립트 개발은 nodejs 프로젝트를 만든 다음, 개발 언어를 타입스크립트로 설정하는 방식으로 진행한다.

 

프로젝트 생성자 관점에서 패키지 설치하기

1. 폴더 만들기

2. 만든 폴더로 이동 후, 터미널에서 npm init (package.json 파일이 생긴다.)

3. npm install -D typescript ts-node (node_modules 디렉터리가 생기고, 각 패키지의 디렉터리들을 확인할 수 있다.)

 

프로젝트 이용자 관점에서 패키지 설치하기

프로젝트를 구현할 때 여러 패키지를 설치하게 되므로 node_modules 디렉터리의 크기가 매우 커진다. 그래서 다른 사람들에게 프로젝트를 전달할 때 node_modules 폴더를 모두 지운다. package.json 파일이 있는 디렉터리의 터미널에서 npm install 또는 npm i 명령어를 실행하면 package.json에 등록된 패키지들이 node_modules 디렉터리에 자동으로 설치된다.

// package.json
{
  "name": "ch02-1",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.js",
  "scripts": {
    "dev": "ts-node src",
    "build": "tsc && node dist"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/chance": "^1.1.3",
    "@types/node": "^20.1.0",
    "@types/ramda": "^0.29.1",
    "ts-node": "^10.9.1",
    "typescript": "^5.0.4"
  },
  "dependencies": {
    "chance": "^1.1.11",
    "ramda": "^0.29.0"
  }
}

 

tsconfig.json 파일 만들기

tsc --init 명령어를 실행하면 타입스크립트 컴파일러의 설정파일인 tsconfig.json 파일이 만들어진다.

// tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "node",
    "target": "es5",
    "baseUrl": ".",
    "outDir": "dist",
    "paths": {
      "*": [
        "node_modules/*"
      ]
    },
    "esModuleInterop": true,
    "sourceMap": true,
    "downlevelIteration": true,
    "noImplicitAny": false,

  },
  "include": [
    "src/**/*"
  ]
}

 


02-2 모듈 이해하기

타입스크립트에서는 index.ts와 같은 소스 파일을 모듈(module)이라고 한다.

소스 파일을 하나로 구현해도 되지만, 코드 관리와 유지보수를 편리하기 하기 위해서 모듈마다 고유한 기능을 구현하는 방식으로 소스코드를 분할한다. 이러한 작업을 모듈화(modulization) 라고 한다.

 

export 키워드

작성한 소스코드를 다른 파일에서 동작하게 하려면 export 키워드로 심벌을 내보낸다.

export 키워드는 interface, class, type, let, const 키워드 앞에도 붙일 수 있다.

 

export default 키워드

export default 키워드는 한 모듈이 내보내는 기능 중 오직 1개에만 붙일 수 있다.

export default가 붙은 기능은 import 문으로 불러올 때 중괄호 없이 사용할 수 있다.

export default interface IPerson {
  name: string,
  age: number
}

 

import 키워드

export 키워드로 내보낸 심벌을 사용하기 위해서 import 키워드로 해당 심벌을 불러온다.

import { 심벌목록 } from '파일 상대 경로'
import * as 심벌 from '파일 상대 경로'

import IPerson from './IPerson'

 

외부 패키지를 사용할 때 import 문

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

ramda는 함수형 유틸리티 패키지이다.

npm i -S chance ramda
npm i -D @types/chance @types/ramda

import Chance from 'chance'
import * as R from 'ramda'

 


02-3 tsconfig.json 파일 살펴보기

콜론(:)을 기준으로 "키:키값" 형태로 작성한다.

// tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "node",
    "target": "es5",
    "baseUrl": ".",
    "outDir": "dist",
    "paths": {
      "*": [
        "node_modules/*"
      ]
    },
    "esModuleInterop": true,
    "sourceMap": true,
    "downlevelIteration": true,
    "noImplicitAny": false,

  },
  "include": [
    "src/**/*"
  ]
}

 

  • compilerOptions

tsc 명령 형식에서 옵션을 나타낸다.

 

  • include

compilerOptions의 대상 파일 목록이다.

 

  • module

타입스크립트 소스코드가 컴파일되어 만들어진 자바스크립트 소스코드는 웹 브라우저와 nodejs 모두 동작해야 한다. 그런데 웹 브라우저와 nodejs는 물리적으로 동작하는 방식이 달라서 자바스크립트 코드 또한 웹 브라우저랑 nodejs에서 다르게 동작한다. 따라서 tsconfig.js 파일에서 module 키는 동작 대상이 웹 브라우저인지 nodejs인지 구분해줘야 한다.

 

웹 브라우저에서 동작: amd

nodejs에서 동작: commonjs

 

  • moduleResolution

module의 키 값이 amd이면 moduleResolution 키 값은 classic

module의 키 값이 commonjs이면 moduleResolution 키 값은 node

 

  • target 키

트랜스파일할 대상 자바스크립트의 버전을 설정한다.

 

  • baseUrl

주로 현재 디렉터리를 의미하는 "."으로 키 값을 설정한다.

 

  • outUrl

baseUrl 설정값을 기준으로 했을 때 하위 디렉터리의 이름이다.

빌드된 결과가 해당 디렉터리에 만들어진다.

 

  • paths

import 문에서 from 부분을 해석할 때 찾아야 하는 디렉터리이다.

 

  • esModuleInterop

chance 패키지를 사용하려면 true로 설정한다.

 

  • sourceMap

sourceMap의 값이 true이면 트랜스파일 디렉터리에 js 파일뿐만 아니라. js.map 파일도 만들어진다.

 

  • downlevelIteration

생성기 구문을 사용하려면 true로 설정한다.

 

  • noImplicitAny

타입을 지정하지 않더라도 오류로 인식하지 않게 하려면 false로 설정한다.

 

 

참고자료

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

01-1 타입스크립트란 무엇인가?

세 종류의 자바스크립트

  • 웹 브라우저에서 동작하는 표준 자바스크립트 ES5(ECMAScript 5)
  • 2015년부터 매년 새로운 버전을 발표하는 ESNext(ES6 이후의 버전)
  • ESNext에 타입(type) 기능을 추가한 타입스크립트(TypeScript)

자바스크립트(동적 언어): 런타임에 타입 결정, 오류 발견

타입스크립트, 자바(정적 언어): 컴파일 타임에 타입 결정, 오류 발견

 

타입스크립트는 누가 만들었나?

타입스크립트는 마이크로소프트가 개발하고 유지하고 있는 오픈소스 프로그래밍 언어로 2012년 말 처음 발표됐다.

구글의 Anglar.js 팀이 앵귤러 버전 2를 타입스크립트로 만들면서 널리 알려졌고, 앵귤러뿐만 아니라 리액트나 뷰 프레임워크에서도 타입스크립트를 사용해 개발되고 있다.

 

자바스크립트에 타입 기능이 있으면 좋은 이유

여러 사람이 협력해 하나의 제품을 개발하기 때문에 다른 개발자가 만들어 놓은 코드를 이용하려고 했을 때 오류가 발생했다면, 오류의 원인이 무엇인지 찾기가 어렵다. 타입스크립트 컴파일러는 문제의 원인이 어디에 있는지 친절하게 알려주기 때문에 코드를 좀 더 수월하게 작성할 수 있다. 따라서 많은 개발자들이 대규모 소프트웨어를 만들 때 타입스크립트를 선호하게 되었다.

 

  • 타입스크립트 소스 코드

타입스크립트를 사용 시, 소스 코드를 실행하기 전에 오류를 알려줘 개발할 때 시간이 오래 걸리더라도 오류 검출이나 유지보수에 용이하다.

아래 사진처럼 컴파일하기 전에 어디에 오류가 있는지 알려준다.

 

 

  • 자바스크립트 소스 코드

타입스크립트와 달리 자바스크립트는 오류 없이 소스 코드를 실행하기 때문에 NaN과 같은 원하지 않는 결과가 나오거나, 컴파일 시 TypeError를 알려준다.

이렇게 되면 오류 검출에 많은 시간을 소요하게 되기 때문에 타입스크립트를 사용하는 것이다.

 

 

트랜스파일

트랜스파일러(transpiler)란, 어떤 프로그래밍 언어로 작성된 소스코드를 또 다른 프로그래밍 언어로 바꿔주는 프로그램을 의미한다. (컴파일과 동일한 뜻으로 사용)

즉, 우리가 사용하는 웹 브라우저는 타입스크립트를 이해하지 못하기 때문에 트랜스파일러를 이용해서 타입스크립트를 자바스크립트로 변환해주어야 한다.

 

ESNext 자바스크립트 소스코드는 바벨이라는 트랜스파일러를 통해 ES5 자바스크립트 코드로 변환된다.

타입스크립트 소스코드는 TSC(TypeScript Compiler)라는 트랜스파일러를 통해 ES5 자바스크립트 코드로 변환된다.

 


01-2 타입스크립트의 주요 문법 살펴보기

ESNext의 주요 문법 살펴보기

타입스크립트는 ESNext 문법을 지원하기 때문에 타입스크립트를 다루기 위해서는 ESNext 문법을 알아야만 한다.

 

1. 비구조화 할당

객체와 배열에 비구조화 할당을 적용할 수 있다.

// 객체
let person = { name: 'Jane', age: 22 }
let { name, age } = person // name = 'Jane', age = 22

// 배열
let array = [1, 2, 3, 4]
let [head, ...rest] = array // head = 1, rest = [2, 3, 4]

 

2. 화살표 함수

function 키워드 대신에 화살표(=>)로 함수를 선언할 수 있다.

function add(a, b) { return a + b }
const add = (a, b) => a + b

 

3. 클래스

객체지향 프로그래밍을 지원한다.

캡슐화, 상속, 다형성

 

4. 모듈

모듈을 사용하면 코드를 여러 개의 파일로 분할해서 작성할 수 있다.

변수, 함수, 클래스 등에 export 키워드를 사용해 모듈로 만들면 다른 파일에서도 사용할 수 있고, 모듈을 사용하고 싶을 때는 import 키워드를 사용한다.

 

5. 생성기

yield 문은 반복자를 의미하는 반복기를 생성할 때 사용한다.

반복기는 독립적으로 존재하지 않고 반복기 제공자(생성기)를 통해 얻는다.

 

6. Promise와 async/await 구문

비동기 콜백 함수를 상대적으로 쉽게 구현할 목적으로 만들어졌다.

 

타입스크립트 고유의 문법 살펴보기

1. 타입 주석과 타입 추론

변수 뒤의 콜론과 타입 이름을 "타입 주석" 이라고 한다.

변수의 타입 부분이 생략되면 대입 연산자의 오른쪽 값을 분석해 왼쪽 변수의 타입을 결정하는 것을 "타입 추론" 이라고 한다.

let n: number = 1

 

2. 인터페이스

interface Person {
    name: string,
    age?: number
}

let person: Person = { name: "Jane" }

 

3. 튜플

배열에 저장되는 아이템의 데이터 타입이 모두 같으면 배열, 다르면 "튜플"이다.

let array: number[] = [1, 2, 3]
let tuple: [boolean, number, string] = [true, 1, 'ok']

 

4. 제네릭 타입

여러 가지 타입을 대상으로 동작할 수 있는 코드를 "제네릭 타입" 이라고 한다.

 

5. 대수 타입

대수 타입이란, 다른 자료형의 값을 가지는 자료형을 의미한다.

대수 타입에는 합집합 타입(&), 교집합 타입(|) 2가지가 있다.

type NumberOrString = number | string
type AnimalAndPerson = Animal & Person

 


01-3 타입스크립트 개발 환경 만들기

타입스크립트의 개발 환경은 nodejs 개발 환경과 동일하다. (타입스크립트는 nodejs 환경에서만 동작한다. = node가 설치되어 있어야 한다.)

nodejs와 웹 브라우저, 에디터만 있으면 개발할 수 있다.

 

타입스크립트 컴파일러 설치

npm: nodejs 패키지 관리자

// 1. 타입스크립트 컴파일러 설치
npm i -g typescript

// 1-1. 설치된 타입스크립트 컴파일러 버전 확인
tsc --version 또는 tsc -v

// 2. 타입스크립트 코드를 ES5로 변환하고, 실행까지 시키는 패키지 설치
npm i -g ts-node

// 2-1. 설치된 ts-node 버전 확인
ts-node --version 또는 ts-node -v

 

tsc hello.ts 명령어를 실행하면 타입스크립트 소스가 tsc에 의해 트랜스파일 되어 hello.js 파일이 생성됐다.

 

 

 

참고자료

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

+ Recent posts