본문으로 건너뛰기

"programming" 태그로 연결된 4개 게시물개의 게시물이 있습니다.

모든 태그 보기

Typescript에서 typeinterface에 대한 유튜브 영상을 보다가 좀 정리해두고 싶어서 글로 남긴다.

예제

// interface 사용
interface User {
name: string;
age: number;
}

// type 사용
type UserType = {
name: string;
age: number;
};

객체 타입을 정의할 때 typeinterface 둘 다 사용할 수 있다.

타입을 확장하는 데는 차이가 조금 있다. 먼저 논리적으로 불가능한 타입을 확장하는 경우를 살펴보자.

// type 확장
type A = {
a: string;
};
type B = {
a: number;
};
type C = A & B; // type C = A & B

// interface 확장
interface A {
a: string;
}
interface B {
a: number;
}
interface C extends A, B {} // Error: Interface C cannot simultaneously extend types 'A' and 'B'

a 프로퍼티가 string 이면서 number 로 선언되어 있기 때문에 C 타입은 논리적으로 불가능하다. type으로 선언된 C의 경우 불가능한 타입임에도 단순 타입간의 intersection으로 표기된다.

반면 interface의 경우 두 타입을 extends 할 수 없다는 에러가 발생한다.

interface를 사용하는 것이 에러를 더 빨리 발견할 수 있다는 점에서 장점이 있다고 생각한다. 또한 타입스크립트가 추론해주는 타입도 intersection에 비해 더 명확하다.


성능 관점 - 벤치마크로 확인하기

또한 두 확장 방식은 성능에 있어서도 차이가 있다. Typescript 공식 레포 wiki에 따르면, ts 컴파일러는 typeintersection 보다 interfaceextends를 더 빠르게 처리한다고 한다.

Interfaces also display consistently better, whereas type aliases to intersections can't be displayed in part of other intersections. Type relationships between interfaces are also cached, as opposed to intersection types as a whole. A final noteworthy difference is that when checking against a target intersection type, every constituent is checked before checking against the "effective"/"flattened" type.

마지막 문장을 보면 큰 객체 타입에 대해서 type을 사용한다면 성능에 꽤 영향이 있을지도 모르겠다는 생각이 들었다.

실제로 이 둘의 성능이 얼마나 차이가 나나 확인해보기 위해 벤치마크를 작성했다.

type 확장 케이스에 대해 실행시킬 코드는 아래와 같다.

import { performance } from "perf_hooks";
import { JSDOM } from "jsdom";

const { window } = new JSDOM("<html><body></body></html>");
const { document } = window;

export function measureTypePerformance(iterations: number): number[] {
const typeTimes: number[] = [];

for (let i = 0; i < iterations; i++) {
const typeStart = performance.now();

for (let j = 0; j < 1000; j++) {
type ExtendedDivType = HTMLDivElement & {
customProperty: string;
};
const divType: ExtendedDivType = Object.assign(
document.createElement("div"),
{
customProperty: `Type Property ${j}`,
}
);
}

const typeEnd = performance.now();
typeTimes.push(typeEnd - typeStart);
}

return typeTimes;
}

HTMLDivElement 정도면 꽤 큰 타입이라고 생각해서 이 타입을 확장하도록 구현했다.

더 자세한 사항은 레포지터리에서 확인할 수 있다.

이렇게 1000개의 type 확장 / interface 확장을 각각 10번 컴파일하여 실행 시간을 비교해보았다.

  • Type Compile Stats (ms)
    • Average: 1369.50085
    • Max: 1488.58892
    • Min: 1241.16438
  • Interface Compile Stats (ms)
    • Average: 1312.45505
    • Max: 1452.09558
    • Min: 1252.57896

결과적으로 interface가 약간 더 빠른 결과를 보였다. 그러나 눈에 띄게 큰 차이가 난다고 보기는 어렵다고 생각한다.

결론

타입 확장에 대해서는 interface가 성능적으로 유리하다. (그러나 큰 차이라고 볼 수는 없다)

가능하면 interface를 먼저 사용하고, type 기능이 필요한 경우에만 type을 사용하자.

요즘 유인동님의 신간인 멀티패러다임 프로그래밍을 읽고 있다. 유인동님의 함수형 강의는 같은 팀 동료 분이 추천해주셔서 들었던 기억이 있는데, 책이 나왔다는 소식에 반가운 마음 + 흥미로운 주제에 구매하게 됐다.

참고로 책의 예제 코드는 이 레포에서 확인할 수 있다. 어떤 디렉터리에 있는지 찾느라 좀 헤매서 남겨본다.

초반 챕터에서 반복자(Iterator) 패턴에 대해 다루는데, 개인적으로 한국어로 iterator를 설명하는 자료 중 가장 명쾌하게 이해할 수 있는 설명이라고 느꼈다. 또한 이 챕터에서 설명하고 있는 내용이 멀티패러다임이라는 주제를 흥미롭게 소개해주는 것 같아 가벼운 글로 소개해보고자 한다.


기본적으로 반복자 패턴은 선언과 동시에 평가되지 않고, 필요한 시점에 평가할 수 있다. 예제를 통해 살펴보자.

const array = [1, 2, 3, 4, 5];
array.reverse();
console.log(array[0], array[1]); // 5 4

반복자를 사용하지 않는 일반적인 방법은 기본적으로 즉시 평가가 일어나며 원본 배열의 mutation이 발생한다. 물론 원본을 복사해둘 수도 있지만 메모리 효율적인 방법은 아니다.

const array = [1, 2, 3, 4, 5];

function reverse(arrayLike) {
let idx = arrayLike.length;
return {
next() {
if (idx-- >= 0) {
return { value: arrayLike[idx], done: false };
} else {
return { value: undefined, done: true };
}
},
};
}

const iterator = reverse(array);
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: 4, done: false }

이 예제에서는 idx라는 변수를 갖는 객체를 리턴하는 함수를 만들었다. reverse()를 통해 객체를 선언하는 시점이 아닌 next()가 호출되는 각각의 시점에 평가가 이루어진다.

즉, 평가는 필요한 시점에 필요한 만큼 효율적으로 이루어진다.

배열의 길이가 커지거나 성능에 대한 요구사항이 엄격해질수록 지연 평가는 큰 장점이 될 수 있다. 성능과는 별개로 원본 배열을 immutable하게 다룰 수 있다는 점에서의 장점도 있다.

여기서 재밌는 지점은 반복자 패턴은 전통적인 객체지향 디자인 패턴이라는 점이다. 그러나 함수가 일급인 자바스크립트에서는 예시 코드에서처럼 함수형 프로그래밍의 장점이 결합되어 더 큰 시너지를 낼 수 있다.

여기서 generator를 사용하여 같은 구현을 명령형 코드로도 구현할 수 있다.

function* reverse<T>(arrayLike: ArrayLike<T>): IterableIterator<T> {
let idx = arrayLike.length;
while (idx--) {
yield arrayLike[idx];
}
}

const array = ["A", "B", "C", "D", "E", "F"];
const reversed = reverse(array);

console.log(reversed.next().value); // F
console.log(reversed.next().value); // E
console.log(reversed.next().value); // D

앞선 reverse 함수는 idx라는 멤버를 가지는 객체를 생성함으로서 반복자를 구현했다. 이 예시에서는 generator를 사용하여 조건문을 사용해 명령형 코드로 같은 기능을 구현했다.

이처럼 다양한 패러다임의 유연한 선택과 그들간의 조합을 통해 문제에 적합한 해결책을 찾을 수 있다.

여기까지 읽고 흥미를 느꼈다면 책을 읽어보는 것도 좋을 것 같다. 초반부는 앞서 링크한 레포에서 읽어볼 수도 있다.

나는 요즘 넘 재미나게 읽고 있어서 추천~


부록

새롭게 추가된 Iterator.prototype의 메서드들로 (map, filter, reduce 등) 반복자에서 지연 평가 로직을 더 간단하게 구현 할 수 있다. 알아두면 좋을 것 같다.

악기 연습할 때 메트로놈을 사용하는데, 웹에서 써본 메트로놈들은 기능적으로 완벽하게 마음에 드는 경우가 잘 없었다.

특히 볼륨 조절이 없는 경우가 많아서 그냥 유튜브에서 xxx BPM metronome 같은 영상을 틀고 연습하는 경우가 많았다.

그런데 유튜브 영상은 BPM을 조절하려면 다른 영상을 서치해서 재생하는 과정이 너무 번거로워서 이럴거면 하나 만들어보자 싶어서 만들었다.

copilot edit 기능을 적극적으로 활용했다. 웹 오디오 API에 대한 지식이 거의 없었는데, copilot이 관련된 코드를 잘 만들어줘서 큰 도움이 됐다. 만들어진 코드를 수정하다 보니 오디오 API에 대해서 이해가 되는 부분도 꽤 있었다.

첫 삽 부터 작동하는 프로토타입을 완성하는 데 2~3시간 정도 사용한 것 같다. 그 이후로도 종종 들여다보고 리팩터 하거나 기능을 추가해주고 있다.

아래 링크에 배포해 두었다!

https://jeonjaewon.github.io/web-metronome/

이건 내비게이션에 필요 없어요

업무하면서 도로명과 지번주소가 혼재되어 있는 데이터를 다룬 적이 있는데, 예를 들면 이런 데이터였습니다.

  • 주소1: 서울특별시 서초구 반포동 20-43 반포자이아파트 101동 101호
  • 주소2: 서울특별시 영등포구 국회대로62길 9 (여의도동)

예시의 주소 1의 경우 지번주소, 주소2는 도로명주소 입니다.

문제는 이 데이터를 토대로 내비게이션 API에 요청을 보내야 했는데, 저희가 사용 중인 API에서는 상세 주소에 대한 처리가 되어 있지 않아서 간혹 응답을 내리지 않는 경우가 있었습니다.

주소 3을 예로 들면 상세주소에 해당하는 반포자이아파트 101동 101호에서 101호는 사실 길찾기에 필요한 정보는 아닙니다. 이걸 포함시켜서 요청하면 길을 제대로 찾지 못하더라구요.

이해를 돕기 위해 주소를 두 가지로 분류해 보겠습니다.

  1. 배송을 위한 주소
  2. 길찾기를 위한 주소

1과 2가 같은 경우도 분명 있지만, 공동주택 등의 경우 정확한 호수를 찾기 위한 상세주소가 더해지기 때문에 표기에 차이가 생깁니다.

제가 겪은 문제의 경우 2만 취급하는 API에 1을 보냈기 때문에 발생한 문제였습니다.

그렇다면 문자열 뒷부분의 상세주소만 제거해서 요청하면 되겠다는 생각이 듭니다. 이제 이 문자열들의 어디서부터가 상세주소인지를 찾아야 합니다.

그렇다면 섞여 있는 도로명주소와 지번주소를 각각 다르게 파싱해야 할까요? 애초에 별도의 칼럼이 없는데 문자열만 보고 이 둘을 구분할 수는 있을까요?


모르겠을 땐 문서를 보기

도로명주소 안내 시스템에 가보면 도로명 주소 소개 pdf 파일을 배포하고 있습니다.

Example banner

눈여겨보아야 할 부분은 가운데의 동/리 지번도로명 건물번호에 대응된다는 점입니다. 자세한 도로명 건물번호 규칙은 아래와 같은데요.

Example banner

이 부분이 중요합니다. 도로명은 붙여 쓰고, 도로명과 건물번호는 띄우기 때문에 이는 기존의 동/리 지번과 공백을 기준으로 호환이 된다고 볼 수 있습니다.

무슨 말인지 앞서 보았던 예시를 다시 보겠습니다.

  • 주소1: 서울특별시 서초구 반포동 20-43 반포자이아파트 101동 101호
  • 주소2: 서울특별시 영등포구 국회대로62길 9 (여의도동)

지번주소의 동/리 지번반포동 20-43, 도로명주소의 도로명 건물번호국회대로62길 9에 해당하는 것을 알 수 있습니다. 그리고 이 주소들은 모두 가운데에 공백 문자를 1개 포함한 문자열입니다.

이 주소들 이후부터는 상세주소가 나오는 것을 확인할 수 있습니다.

즉, 도로명이냐 지번이냐에 관계 없이 단순히 문자열을 공백을 기준으로 앞에서부터 4번 취하면 상세주소를 제거한 주소를 얻을 수 있습니다.

"서울특별시 서초구 반포동 20-43 반포자이아파트 101동 101호"
.split(" ")
.slice(0, 4)
.join(" ");
// '서울특별시 서초구 반포동 20-43'

"서울특별시 영등포구 국회대로62길 9 (여의도동)"
.split(" ")
.slice(0, 4)
.join(" ");
// '서울특별시 영등포구 국회대로62길 9'

이제 이 문자열이 신주소인지 구주소인지와는 관계 없이 간단한 방법으로 상세주소를 제거할 수 있습니다.