이번 장에서는 타입스크립트 언어가 제공하는 반복기와 반복기 제공자, 그리고 생성기에 대해 알아보자.
6-1 반복기 이해하기
실습 프로젝트 설정
이번 장에서 소개하는 예제는 Node.js 설정과 tsconfig.json 파일에 별도의 설정이 필요하다. 먼저 다음 명령을 터미널에서 실행하자.
npm init -y
npm i -D typescript ts-node @types/node
mkdir src
다음, tsc —init 명령으로 tsconfig.json 파일을 생성하고 다음 내용으로 대체하자.
// tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"target": "es2015",
"moduleResolution": "node",
"outDir": "dist",
"baseUrl": ".",
"sourceMap": true,
"downlevelIteration": true,
"noImplicitAny": false,
"paths": { "*": ["node_modules/*"] }
},
"include": ["src/**/*"]
}
반복기와 반복기 제공자
앞서 5장에서 for…in과 for…of 문을 공부했었다. 이 중 for…of 문은 다음 코드처럼 타입에 무관하게 배열에 담긴 값을 차례로 얻는 데 활용한다.
const numArray: number[] = [1,2,3]
for(let value of numArray)
console.log(value) // 1 2 3
const strArray: string[] = ["hello", "world", "!"]
for(let value op strArray)
console.log(value) // hello world !
for…of 문은 다른 프로그래밍 언어에서도 반복기 라는 주제로 흔히 볼 수 있다. 대부분 프로그래밍 언어에서 반복기는 다음과 같은 특징이 있는 객체이다.
- next라는 이름의 메서드를 제공한다.
- next 메서드는 value와 done이라는 두 개의 속성을 가진 객체를 반환한다.
다음 코드에서 createRangeIterable 함수는 next 메서드가 있는 객체를 반환하므로 이 함수는 반복기를 제공하는 역할을 한다. 이러한 반복기를 제공하는 역할을 하는 함수를 반복기 제공자라고 한다
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 }
}
}
}
다음 코드는 createRangeIterable 함수가 제공하는 반복기를 사용하는 예이다.
import { createRangeIterable } from './createRangeIterable'
const iterator = createRangeIterable(1, 3 + 1)
while (true) {
const { value, done } = iterator.next()
if (done) break
console.log(value) // 1 2 3
}
- createRangeIterable 함수를 호출해 반복기를 얻고 iterator 변수에 저장한다.
- 이처럼 반복기 제공자를 호출해야만 얻을 수 있다.
- while 문에서 done 값을 true로 반환할 때까지 iterator 변수의 next 메서드를 반복 호출하면서 value값을 얻는다.
반복기는 왜 필요한가?
앞 코드의 실행 결과를 보면 1부터 3까지 정수를 출력한다. 즉, iterator.next 메서드가 반복 호출될 때마다 각기 다른 값이 출력된다. 반복기 제공자가 생성한 값을 배열에 담아서 출력하지 않고, 마치 for 문을 돌면서 값을 콘솔 출력문으로 찍어낸 듯한 모습이다. 반복기 제공자는 이처럼 어떤 범위의 값을 한꺼번에 생성해서 배열에 담지 않고 값이 필요할 때만 생성한다.
앞서 5장에서 봤던 range 함수
export const range = (from, to) => from < to ? [from, ...range(from + 1, to)] : []
createRangeIterable 함수는 값이 필요한 시점에 비로소 생성되지만, range 함수는 값이 필요한 시점보다 이전에 미리 생성한다는 차이가 있다. 즉, 시스템 메모리의 효율성을 보면 createRangeIterable 함수가 메모리를 훨씬 적게 소모한다.
for…of 문과 [Symbol.iterator] 메서드
range 함수는 for…of 구문의 of 뒤에 올 수 있다.
import {range} from "./range"
for(let value of range(1, 3 + 1))
console.log(value) // 1 2 3
하지만 다음 코드처럼 작성한 createRangeIterable 함수를 for…of 문에 적용하면 Symbol.iterator 메서드가 없다라는 오류가 발생한다.
import { createRangeIterable } from './createRangeIterable'
const iterable = createRangeIterable(1, 3 + 1)
for(let value of iterable)
console.log(value)
이 오류는 createRangeIterable 함수를 RangeIterable 처럼 클래스로 구현해야 한다는 것을 의미한다.
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 }
}
}
}
}
createRangeIterable 함수와 달리 RangeIterable 클래스는 다음 코드에서 보듯 range 함수처럼 for…of 구문의 of 뒤에 올 수 있다.
import { RangeIterable } from './RangeIterable'
const iterable = new RangeIterable(1, 3 + 1)
for (let value of iterable) {
console.log(value) // 1 2 3
}
Iterable<T>와 Iterator<T>인터페이스
타입스클비트는 반복기 제공자에 제네릭 인터페이스를 사용할 수 있다. **Iterable<T>**는 자신을 구현하는 클래스가 [Symbol.iterator] 메서드를 제공한다는 것을 명확하게 알려주는 역할을 한다.
class 구현클래스 implements Iterable<생성할 값의 타입>{}
또한, **Iterator<T>**는 반복기가 생성할 값의 타입을 명확하게 해준다.
[Symbol.iterator](): Iterator<생성할 값의 타입>{}
다음 코드를 통해 반복기 제공자를 타입스크립트가 제공하는 Iterable<T> 와 **Iterator<T>**를 사용해 구현한 코드이다.
export class StringIterable implements Iterable<string> {
constructor(private strings: string[] = [], private currentIndex: number = 0) {}
[Symbol.iterator](): Iterator<string> {
const that = this
let currentIndex = that.currentIndex,
length = that.strings.length
const iterator: Iterator<string> = {
next(): { value: string; done: boolean } {
const value = currentIndex < length ? that.strings[currentIndex++] : undefined
const done = value == undefined
return { value, done }
}
}
return iterator
}
}
다음처럼 테스트 코드를 작성해 실형하면 StringIterable 클래스의 strings 속성에 담긴 열의 아이템을 하나씩 출력한다.
import { StringIterable } from './StringIterable'
for (let value of new StringIterable(['hello', 'world', '!']))
console.log(value) // hello world !
6-2 생성기 이해하기
ESNext 자바스크립트와 타입스크립트는 yield라는 키워드를 제공한다. yield는 마치 return 키워드처럼 값을 반환한다. yield는 반드시 function* 키워드를 사용한 함수에서만 호출할 수 있다. 이렇게 function* 키워드로 만든 함수를 생성기라고 한다.
다음 코드에서 function* 키워드로 만든 generator 함수가 있다. generator 함수의 몸통은 yield문을 3회 반복해서 호출되도록 구현했다.
export function* generator() {
console.log('genertor started...')
let value = 1
while (value < 4) yield value++
console.log('genertor finished...')
}
다음은 이 함수를 테스트하는 코드이다.
import { generator } from './generator'
for (let value of generator())
console.log(value)
// generator started...
// 1
// 2
// 3
// generator finished...
흥미로운 점은 배열을 대상으로 실행했던 for…of문과 같은 출력을 보인다.
setInterval 함수와 생성기의 유사성
생성기가 동작하는 방식을 세미코루틴(semi-coroutine, 반협동 루틴)이라고 한다. 세미코루틴은 타입스크립트처럼 단일 스레드로 동작하는 프로그래밍 언어가 마치 다중 스레드로 동작하는 것처럼 보이게 하는 기능을 한다.
자바사크립트가 기본으로 제공하는 setInterval 함수를 사용해 세미코루틴의 동작 방식을 알아보자.
const intervalID = setInterval(콜백 함수, 호출 주기);
setInterval 함수는 지정한 주기로 콜백 함수를 계속 호출해 준다. 무한히 반복하지만 clearInterval로 멈출 수 있다.
다음 코드는 setInterval 로 1초 간격으로 1,2,3 을 출력하는 예제이다.
const period = 1000
let count = 0
console.log('program started...')
const id = setInterval(() => {
if (count >= 3) {
clearInterval(id)
console.log('program finished...')
} else console.log(++count)
}, period)
프로그램의 출력 내용만 보면 앞에서 살펴본 생성기 방식과 구분할 수 없을 정도로 비슷하다. 하지만 setInterval 함수가 동작하는 구조는 C++ 언어의 스레드가 동작하는 방식과 흡사하다. 즉, “program started”를 출력하고 setInterval을 동작시킨 부분이 메인 스레드, setInterval의 콜백 함수는 작업 스레드를 떠올리게 한다.
생성기는 이처럼 일반적인 타입스크립트 코드와는 좀 다른 방식으로 동작한다.
function 키워드*
앞에서 본 generator 함수는 지금까지 본 함수와 비교했을 때 다음 두 가지의 차이가 있다.
- function* 키워드로 함수를 선언한다.
- 함수 몸통 안에 yield 문이 있다.
즉, 생성기는 오직 function* 키워드로 선언해야 하므로 화살표 함수로는 생성기를 만들 수 없다. 또한, 생성기는 반복기를 제공하는 반복기 제공자로서 동작한다.
yield 키워드
yield는 연산자 (operator) 형태로 동작하며 두 가지 기능을 한다.
- 반복기를 자동으로 만들어 준다.
- 반복기 제공자 역할도 수행한다.
function* 키워드를 사용해 생성기 형태로 rangeGenerator 함수를 만들어보자.
export function* rangeGenerator(from: number, to: number) {
let value = from
while (value < to) {
yield value++
}
}
테스트 코드:
import { rangeGenerator } from './rangeGenerator'
// while 패턴으로 동작하는 생성기
let iterator = rangeGenerator(1, 3 + 1)
while (1) {
const { value, done } = iterator.next()
if (done) break
console.log(value) // 1 2 3
}
// for...of 패턴으로 동작하는 생성기
for (let value of rangeGenerator(4, 6 + 1)) console.log(value)
반복기 제공자의 메서드로 동작하는 생성기 구현
생서기는 반복기를 제공하는 반복기 제공자로서 동작하므로, 생성기를 사용하면 앞에서 사용했던 StringIterable 클래스를 다음처럼 간결하게 구현할 수 있다.
export class IterableUsingGenerator<T> implements Iterable<T> {
constructor(private values: T[] = [], private currentIndex: number = 0) {}
[Symbol.iterator] = function*() {
while (this.currentIndex < this.values.length)
yield this.values[this.currentIndex++]
}
}
테스트 코드:
import { IterableUsingGenerator } from './IterableUsingGenerator'
for (let item of new IterableUsingGenerator([1, 2, 3])) {
console.log(item) // 1 2 3
}
for (let item of new IterableUsingGenerator(['hello', 'world', '!'])) {
console.log(item) // hello world !
}
yield 키워드*
타입스크립트는 yield 키워드 뒤에 을 붙인 yield 키워드도 제공한다.
function* gen12() {
yield 1
yield 2
}
export function* gen12345() {
yield* gen12()
yield* [3, 4]
yield 5
}
- yield는 단순히 값을 대상으로 동작하지만 yield* 는 다른 생성기나 배열을 대상으로 동작한다.
테스트 코드 :
import { gen12345 } from './yield-star'
for (let value of gen12345())
console.log(value) // 1 2 3 4 5
yield 반환값
yield 연산자는 값을 반환한다. 반환값을 select라는 변수에 저장해보자.
export function* gen() {
let count = 5
let select = 0
while (count--) {
select = yield `you select ${select}`
}
}
export const random = (max, min = 0) => Math.round(Math.random() * (max - min)) + min
yield 연산자의 반환값은 반복기의 next 메서드 호출 때 매개변수에 전달하는 값이다. 다음 테스트 코드를 통해 next 메서드 호출 때 난수를 생성해 전달해 보자.
import { random, gen } from './yield-return'
const iter = gen()
while (true) {
const { value, done } = iter.next(random(10, 1))
if (done) break
console.log(value)
}
// you select 0
// you select 5
// you select 4
// you select 7
// you select 7
코드를 실행하면 첫 줄 외에 다른 줄은 모두 난수가 출력된다. 첫 줄은 항상 “you select 0” 이 출력된다. 이는 select 변수를 0으로 설정했기 때문이다. 실행 결과는 이전에 next 메서드가 전달한 값이 다시 gen 함수의 내부 로직에 의해 현재의 value값이 되어 출력된다.
'Books > Do It 타입스크립트 프로그래밍' 카테고리의 다른 글
8장 함수 조합의 원리와 응용 (0) | 2023.01.04 |
---|---|
7장 비동기 콜백 함수 (0) | 2022.12.30 |
5장 배열과 튜플 (0) | 2022.12.11 |
4장 함수와 메서드 (0) | 2022.11.29 |
3장 객체와 타입 (0) | 2022.11.29 |