타입스크립트 복습 - 3

Stulta Amiko·2022년 7월 21일
0
post-thumbnail

Promise / async / await

먼저 Promise에 대해서 보면
MDN에서는 프로미스를 다음과 같이 설명하고있다.

Promise 객체는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타냅니다.

프로미스는 resolve와 reject를 반환한다.

먼저 프로미스를 사용하는 이유에 대해 알려면 비동기식 코드와 동기식 코드에 대해서 알아야하는데
먼저 동기식으로 작동하게 되면 단일스레드로 작동하는 방식이라 어느 한작업이 끝날때까지 다른작업을 실행하지 않게된다.
하지만 이런방식으로 실행되게 된다면 작업을 지연시키는 주요원인이 될것이다.

따라서 현재 웹의 대부분은 비동기식 코드를 이용하고 있다.
비동기식 코드는 다중스레드처럼 동작하게 만들어준다. 따라서 효율적인 작업수행이 가능해진다.

readFile('./package.json',(err: Error,buffer: Buffer)=>{
    console.log('read package.json using asynchronous api')
    console.log(buffer.toString())
})

위는 비동기식 코드를 구현한것이다. package.json내용을 받아오는 코드이고 콜백을 받는데 첫번째로 error를 두번째로 버퍼를 받는다. 그리고 버퍼를 string화 시켜서 출력시킨다.
이런방식이 효율적인 작업수행을 하는데 도움이 되긴하지만 이런식으로 콜백을 계속 엮게된다면 콜백지옥이라는것이 발생하게 된다.
비동기코드를 구현하려고 콜백안에 콜백을 또 그안에 콜백을 계속반복하게 되는 코드를 말하는 것이다. 이런 단점을 보완하기 위해서 존재하는게 바로 Promise이다.

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

위와 동일하게 파일을 읽는 코드를 작성하되 프로미스를 이용해서 작성을 해봤다.

프로미스는 일종의 클래스 이기때문에 new 키워드를 이용한다. 그리고 resolve와 reject를 콜백으로 받는다.

readfile을 했을때 오류가 나면 reject를 반환하고 정상적으로 실행이 되었을 때에는 resolve를 반환하게 된다.

readFilePromise('./package.json')
    .then((content: string)=>{
        console.log(content)
        return readFilePromise('./tsconfig.json')
    })
    .then((content: string)=>{
        console.log(content)
        return readFilePromise('.')
    })
    .catch((err: Error)=>console.log('err:',err.message))
    .finally(()=>'end')

위 프로미스 코드를 이용하면 먼저 package.json을 읽어드린후에 버퍼를 tostring 한것을 출력한다 그다음에 tsconfig.json을 읽은후에 이를 출력하고 .을 읽으려고 하면 오류가 발생하니 catch로 오류메시지를 출력한 후에 finally 메서드로 프로그램을 종료하는 코드이다.


async,await

async function 선언은 AsyncFunction객체를 반환하는 하나의 비동기 함수를 정의합니다. 비동기 함수는 이벤트 루프를 통해 비동기적으로 작동하는 함수로, 암시적으로 Promise를 사용하여 결과를 반환합니다. 그러나 비동기 함수를 사용하는 코드의 구문과 구조는, 표준 동기 함수를 사용하는것과 많이 비슷합니다.

MDN의 설명은 위와같다.

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

const test2 = async() =>{
    let value = await 'hello'
    console.log(value)
    value = await Promise.resolve('hello')
    console.log(value)
}

test1()
test2()

위 코드를 실행하게 되면 1 1 hello hello가 아닌 1 hello 1 hello가 나오게 된다.

이를 의도한 대로 1 1 hello hello가 나오게 하려면 코드를 살짝 수정해주면 된다.

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

마지막줄을 위와같은 형식으로 수정해주면되는데 test1함수가 끝난 후에 test2 함수를 실행하기 때문에 의도한 대로 1 1 hello hello가 나오게된다.

그리고 await은 async 안에서만 사용할 수 있다.
await은 프로미스를 기다리기 위해서 사용하는 연산자이다.

async는 봐도봐도 잘 모르겠다 하지만 이는 다음코드와 같다고 한다.

async function foo() {
    return 1
}

async로 작성된 foo 함수가 있다.

function foo() {
    return Promise.resolve(1)
}

그리고 프로미스로 작성된 foo 함수가 있는데
이 둘은 같은 함수이다.

위 함수에서 await이 사용되지 않은 모습을 볼 수있는데
이는 동기적으로 작동한다는 것을 의미한다.

await 문이 없는 async 함수는 동기적으로 실행된다
하지만 await 문이 있다면 async 함수는 항상 비동기적으로 완료된다.
라고 MDN에 나와있다.

고차함수

MDN에서는 고차함수를 다음과 같이 설명한다.

함수를 반환하는 함수를 고차 함수라고 부릅니다

일반적으로 볼때 한번 값을 반환하는 함수가 있다 이게 보통 우리가 아는 일반적인 함수이다.
그리고 이걸 1차 고차함수라고 부르는것 같다.
2차 고차함수는 함수가 함수를 리턴하는것이다.

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

const inc: FirstOrderFunc<number,number> = (x: number) => x+1

console.log(inc(1))

1차 고차함수의 예시를 보면 위 코드와 같다. 먼저 타입 별칭으로 함수를 만들어 주는데 이게 1차 함수라고 볼수 있을것이다.
그리고 구현을 할때 inc로 구현을 하는데 제네릭에 number를 두개 넣은것을 볼 수있다. 따라서 위 함수는 파라미터로 number를 받고 반환값도 number를 준다는 뜻이다.

type FirstOrderFunc<T,R> = (T) => R
type SecondOrderFunc<T,R> = (T) => FirstOrderFunc<T,R>

const add: SecondOrderFunc<number,number> = 
    (x: number): FirstOrderFunc<number, number> =>
    (y: number): number => x+y

console.log(add(1)(2))

위 코드를 처음 봤을때는 이해가 잘 되지않아서 그냥 넘어갔는데 복습을 하니깐 알 것같다. 확실히 복습이 중요하긴 한거같다.

먼저 첫번째 타입별칭으로 나오는 FirstOrderFunc는 위에서 다루듯이 평범한 1차 고차 함수이다.
그리고 SecondOrderFunc가 있는데 이는 함수를 리턴한다.
따라서 이는 2차 고차 함수임을 알 수 있다.

위 함수는 파라미터를 적을때 ()()이런식으로 적는다.
그래서 결과로 3이 나오는것을 알 수있다.

type FirstOrderFunc<T,R> = (T) => R
type SecondOrderFunc<T,R> = (T) => FirstOrderFunc<T,R>
type ThirdOrderFunc<T,R> = (T) => SecondOrderFunc<T,R>

const add: ThirdOrderFunc<number,number> = 
    (x: number): SecondOrderFunc<number,number> =>
    (y: number): FirstOrderFunc<number,number> =>
    (z: number): number => x+y+z


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

다음은 3차 고차함수이다. 2차고차함수를 무리없이 이해했다면 사실 더이상 어려운점이 보이지 않는다.

type FirstOrderFunc<T,R> = (T) => R
type SecondOrderFunc<T,R> = (T) => FirstOrderFunc<T,R>
type ThirdOrderFunc<T,R> = (T) => SecondOrderFunc<T,R>

const add3: ThirdOrderFunc<number,number> = 
    (x: number): SecondOrderFunc<number,number> =>
    (y: number): FirstOrderFunc<number,number> =>
    (z: number): number => x+y+z

const add2: SecondOrderFunc<number,number> =
    (x: number): FirstOrderFunc<number,number> =>
    (y: number): number => x+y

const add1: FirstOrderFunc<number,number> = (x: number) => x

const add_1: FirstOrderFunc<number,number> = add2(1)
const add_2: SecondOrderFunc<number,number> = add3(1)

console.log(add_2(2)(3),add_1(2))

살짝 응용해서 보자면 이런 코드가 나온다. 원래대로라면 add3는 3차 고차함수 이기때문에 파라미터가 세번들어가야한다. 하지만 미리 정해놨기때문에 두개만 넣어도 오류가 나지 않는다 add_1의 경우도 마찬가지로 add2에 미리 한개의 파라미터를 정해놨기 때문에 문제가 되지 않는다. 위 코드를 실행하면 1+2+3 = 6 이 나오고 뒷 코드는 1+2 = 3 이 나오게된다.


클로저

클로저 라는게 있다. 클로저는 어느 한 언어에 국한된 개념이 아니라
함수형 프로그래밍에서 유효한 개념이다.
먼저 타입스크립트의 가장 중요한 핵심은 자바스크립트에서 시작하기 때문에 MDN의 설명을 참조하자면

클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.

MDN에서는 위와 같이 설명하고 있다
클로저는 이제 유효범위에 대해서 다루는 지정자? 같은 개념이다.

const makeNames = (): () => string =>{
    const names = ['jack','jane','smith']
    let index = 0
    return (): string => {
        if(index == names.length)
            index = 0
        return names[index++]
    }
}

const makeName: () => string = makeNames()
console.log(
    [1,2,3,4,5,6].map(n=>makeName())
)

먼저 앞에서 한번다뤘던 예제이다.
makeNames 함수는 string을 반환하는 익명함수를 가진 2차 고차함수이다.

잠시 위 예제를 다루기 전에 다루지않았던 내용을 봐야겠다.

function add(x: number): (number) => number{ // 바깥쪽 유효범위
    return function(y: number): number{ // 안쪽 유효범위
        return x+y // 클로저
    } //안쪽 유효범위 끝
} //바깥쪽 유효범위 끝

const add1 = add(1) // 1
const result = add1(2) // 2

안쪽의 유효범위만 봤을때 x는 이해할수 없는 변수이다.
이를 자유변수라고 부른다.
하지만 이는 컴파일 될때 문제가 생기는것은 아니다.
클로저는 다른말로 지속되는 유효 범위라고 부르는데
주석에 있는 1의 add1을 보게된다면 다음과 같이 add함수를 호출 하더라도 변수 x가 메모리에서 해제되지 않기 때문이다
출력시켜도 비정상적으로 출력된다. 2차고차함수인데 파라미터가 한번 들어갔기 때문에
주석 2를 보면 저런식으로 되야 비로소 메모리가 해제가 된다.
값을 발생시켜야 메모리가 해제되는 유효범위를 클로저 라고 한다.
개념이 더럽게 난해하다.

이제 위에서 본 예제를 다시보자
index와 names는 return 하는 함수안에서는 자유변수이다.
함수내부에서 index는 다시 0이 되는데 이 메모리가 해제가 되지않는것 같다.

0개의 댓글