TIL

241128 TIL

GoJay 2024. 11. 29. 02:46
  • 인프런 <한 입 크기로 잘라먹는 타입스크립트>
    • 제네릭 함수는 함수에 전달되는 인수의 타입에 따라 반환 값의 타입을 가변적으로 바꿔주는 함수를 의미한다.
    • 기본적으론 인수에 number가 들어가면 number, string이 들어가면 string, number[]가 들어가면 number[]가 나온다. 인수에 들어간 타입이 반환 값에 그대로 적용된다.
    function func<T>(value: T) {
      return value
    }
    
    let num = func(10); // 매개 변수 숫자형, 반환된 num 또한 숫자형
    let str = func('string'); // 매개 변수 문자열, 반환된 num 또한 문자열
    let bool = func(true); // 매개 변수 불리언, 반환된 num 또한 불리언
    • 물론, 전달된 매개 변수와 다른 반환 값 타입을 갖도록 제네릭 함수를 사용할 수도 있다. 사용 방법은 다음과 같다.
    function func<T>(value: T) {
      return value
    }
    
    let tuple = func<[number, number, number]>([1, 2, 3]) // number[] 타입이 나와야 하지만, 명시적으로 반환 값 튜플 타입으로 정의
    • 제네릭 타입 사용에 있어 여러 응용이 필요한 케이스가 있기 때문에, 제네릭 변수와 타입의 특징을 적절히 고려해서 대응하는 게 필요하다.
    // 두 개의 매개 변수가 있는데 각각의 타입이 다를 때: 제네릭 타입 변수를 2개 사용
    function swap<T, U>(a: T, b:U) {
      return [b, a]
    }
    
    // 타입 변수가 unknown이기 때문에 인덱싱, 메서드, 정적 프로퍼티 사용 등에 있어 어려움이 있을 때: 다양한 제네릭 문법을 통해 우회해서 사용
    /** length 프로퍼티를 갖는 타입들만 선택해야 하는 경우 **/
    function getLength<T extends { length: number }>(value: T){
      return value.length
    }
    
    /** 타입이 서로 다른 요소가 담겨있는 배열의 0번째 인덱스를 읽고 싶은 경우 **/
    function returnFirstValue<T>(value: [T, ...unknown[]]){
      return value[0]
    }
    
    /** map 메서드의 타입 정의 직접 해보기 **/
    function myMap<T, U>(arr: T[], callback: (item: T) => U) {
      const result = [];
      for (let i = 0; i < arr.length; i++) {
        result.push(callback[arr[i]]);
      }
      return result;
    }
    
    /** forEach 메서드의 타입 정의 직접 해보기 **/
    function myForEach<T>(arr: T[], callback: (item: T) => void){
      for (let i = 0; i < arr.length; i++) {
        callback(arr[i]);
      }
    }
    • 제네릭 인터페이스를 사용해 객체의 프로퍼티 타입을 정의할 땐 땐 꺽쇠를 사용해서 타입이 무엇인지 지정해줘야 한다. interface KeyPair<K, V> 형식으로 제네릭 인터페이스 타입을 정의했다면, 사용할 땐 let keyPair: KeyPair<string, number> = {} 같이 타입을 지정해줘야 한다.
    • 인덱스 시그니처와 제네릭 인터페이스를 함께 사용해 객체의 타입을 유연하지만 안전하게 사용할 수 있다.
    interface Map<V> {
      [key: string]: V;
    }
    
    let stringMap: Map<string> = {
      key: 'value',
    };
    • 타입 좁히기가 필요한 상황에서(interface User { profile: Student | Developer }) 제네릭을 적절하게 사용하면 굳이 분기문을 만들지 않고 타입을 좁힐 수 있다(function func(user: User<Student>) { /** ...Developer는 실행되지 않음 **/ }).
    • 클래스 제네릭은 클래스 생성 시 클래스 명에 class List<T> { ... } 형태로 타입 변수를 붙여주는 식으로 사용한다. 클래스에선 constructor 초기화를 위한 위한 인수를 전달할 때 알아서 타입을 추론해 준다(제네릭 인터페이스, 제네릭 타입 등과 다르다). 그래서, new List<number>([1, 2, 3])와 같이 명시적으로 타입을 적어주지 않고 new List([1, 2, 3])로 사용해도 문제없이 동작한다.
    • Promise 비동기 처리에서도 비동기 사용이 가능하다. new Promise<number>(/** ...callback **/)라고 Promise 객체를 정의하면 요청이 성공했을 때의 응답인 resolve에는 number 타입의 값만 담길 수 있다. 만약에 제네릭 타입을 따로 정의하지 않으면 타입스크립트에서 resolveunknown 타입이 되어 어떠한 처리도 할 수 없는 상태가 돼버린다.
    • 비동기 처리의 결과가 reject일 때 전달하는 error는 제네릭으로도 타입을 별도로 지정할 수 없다. 그래서, Promise 객체에 catch 메서드로 체이닝 하는 에러 처리 구문의 error는 별도의 타입 좁히기(e.g. if (typeof err === 'string') 또는 if ('message' in err) 등)를 프로젝트 상황에 맞게 잘 사용해줘야 한다.
    • 객체나 배열에서 특정 프로퍼티(인덱스)의 값이 갖는 타입에 접근해서 확인하는 걸 인덱스드 액세스 타입이라고 한다.
    • 객체의 경우, 인터페이스로 정의된 타입이 있다면 Post['author']와 같이 프로퍼티의 이름을 대괄호로 감싸는 식으로 접근해 타입을 활용할 수 있다. 이때 author는 문자열 프로퍼티가 아니라 리터럴 문자열 타입으로 봐야 한다. 대괄호 안에는 값이 아닌 타입 정의가 와야 하며, 만약에 Post[string]이라고 하면 타입이 문자열인 모든 프로퍼티의 타입이 유니언으로 묶이게 될 것이다.
    • 배열은 타입 별칭을 이용해 타입을 표현할 수 있고, PostList[number]처럼 숫자형을 인덱스로 쓸 것이라는 걸 표기해 주면 특정 인덱스의 타입을 표현할 수 있다.
    • 튜플은 튜플의 길이와 인덱스 별 타입을 사전에 지정할 수 있기 때문에 Tup[0]처럼 특정 인덱스로 접근하여 타입을 확인할 수 있다. 배열과 마찬가지로 Tup[number]처럼 인덱스 위치에 숫자형 자료형을 넣어서 처리해 줄수도 있다.
    • interface Person { name: string; age: number }로 타입을 정의해 주고, keyof Person을 해주면 'name' | 'age'의 유니언 타입이 된다. 만약에 프로퍼티가 무수히 늘어나더라도 keyof 연산자는 정의된 타입들의 키(프로퍼티)들의 리터럴들을 유니언 값으로 묶어서 처리해 준다.
    • typeof 연산자는 조건문에서 쓸 땐 typeof obj === 'object'처럼 특정 값의 타입을 가리키지만, interface Person = typeof obj처럼 사용하면 특정 값의 타입을 정의하는 데 활용될 수 있다. 이렇게 하면 특정 값의 프로퍼티가 추가-변경되더라도 interface도 함께 변하기 때문에 편리할 수 있다. 다만, 타입스크립트의 활용 목적을 생각해 봤을 땐 엄격한 타입 검사를 위해 타입을 정의해 두고 특정 값의 타입을 거기에 맞춰서 검사하는 방식이기 때문에, 잘못 활용하면 주객이 전도될 수 있을 것 같다는 개인적인 우려가 드는 방식이다.
    • 맵드 타입은 타입스크립트의 고급 타입으로, 정의되어 있는 객체의 타입에 접근하여 원하는 형태로 타입을 수정해 주는 문법이다. [key in keyof User]: number 같은 형식으로 사용하며, :을 기준으로 왼쪽은 프로퍼티의 타입을 변경할 정의된 타입 인터페이스를, 오른쪽에는 수정해서 지정할 타입을 작성한다. 필요에 따라 옵셔널 연산자 ?를 추가해거나, 모든 프로퍼티를 readonly로 바꿔주는 등 변화를 줄 수 있다.
    • 리터럴 형태로 정의한 타입을 ${Color}-${Animal} 형태의 템플릿 리터럴로 사용해 줄 수 있다. 템플릿 리터럴을 사용하면 리터럴로 정의된 타입을 원하는 위치에 원하는 형태로 사용해 주는 게 가능하다. 자주 사용되는 문법은 아니다.
  • 타입스크립트 공식문서
    • 제네릭 함수란, 함수의 입력 값과 출력 값의 상관관계를 표시하기 위해 사용되는 문법이다. 기본적으론 function {함수명}<타입>(파라미터: 타입) 형태로 사용한다.
    • 많은 경우 제네릭 함수를 쓰지 않아도 되는데 제네릭을 사용하는 경우가 있다. 제네릭의 목적은 여러 값의 타입을 연관시키는 것이 주 용도이다. 그러한 필요가 있는 곳에 사용하는 게 좋고, 또 사용하더라도 최소한으로 사용하는 것이 함수의 동작을 예측 가능하게 하는 데 유리하다. 제네릭을 다양한 방식으로 확장시키면서 너무 많이 쓰는 것은 코드의 가독성을 해친다.
    • 타입 정의하는 방법에 '교차 타입(Intersection)'이라는 게 있다. 정의된 두 개 이상의 타입을 & 연산자로 묶어서 사용한다.
    • interface는 프로퍼티 이름은 같지만 정의된 타입은 서로 다른 두 개의 인터페이스에 대해 에러를 발생시킨다.
    • 하지만, 교차 타입은 프로퍼티 이름이 같지만 서로 다른 타입으로 정의된 두 개의 타입 정의에 대해 에러를 발생시키지 않고 'never' 타입인 것으로 반환한다.
    • 튜플 타입은 배열의 길이와, 인덱스 별 타입을 어느 정도 strict 하게 정해둔다는 특징이 있다. 이런 특징이 있는 관계로, 튜플 타입은 readonly 연산자와 함께 사용하기에 좋은 타입이다.
    • 제네릭 문법을 사용하면 여러 타입 또는 자신만의 타입을 유연하게 적용할 수 있는 컴포넌트를 사용할 수 있다. 타입을 불문하고 사용할 수 있게 한다고 해서 '제네릭'이라는 이름이 붙은 것 같다. 재사용 가능한 컴포넌트는 소프트웨어 엔지니어링의 중요한 과제이며, 이러한 관점에서 제네릭의 중요도는 매우 크다고 할 수 있다.
    • 제네릭에는 타입 변수가 사용된다. 타입 변수는 사용자가 준 값의 타입을 캡처하고, 해당 정보를 나중에 호출된 곳에서 사용할 수 있게 해 준다.
    • 타입 변수가 사용된 제네릭 함수에서 타입스크립트는 전달된 값을 통해 적절하게 타입을 추론해 준다. 타입 변수는 말 그대로 '변수'이기 때문에, 초기 값을 전달하면 타입스크립트는 타입을 추론해 변수에 할당하고, 할당된 값으로 함수 내부에서 동작하게 만든다.
    • 맵드 타입으로 프로퍼티 타입의 조건을 변환할 때 -를 써주면 기존에 있던 조건을 제거하는 식으로 동작한다. 예를 들어 -readonly처럼 readonly 프로퍼티 앞에 -를 붙여주면 기존에 있던 readonly 속성을 제거해 준다.
  • 감정 일기장 with React, TypeScript
    • 간단한 CRUD 기능이 있는 감정 일기장 프로젝트를 타입스크립트를 써서 만들어보고 있다.
    • 확실히 강의와 공식 문서 튜토리얼에서 '보기만' 했던 내용은 진짜 내 것이 아니었다. 막상 써보려니 막막할 뿐.
    • 그래도 하나씩 차근차근 공부한 내용 더듬어가며 내일까진 마무리해봐야겠다.
  • 책 <자바스크립트 + 리액트 디자인 패턴>
    • 다년간 수많은 사람에 의해 다양한 문제를 소프트웨어로 해결해 오던 과정이 반복되면서, 특정 주제에 적용할 수 있는 재사용 가능한 템플릿들이 많이 생겼다. 이러한 경험에 의한 산물을 '패턴'이라고 한다.
    • 패턴은 검증되었고, 재사용성이 높고, 직관적이다. 아무래도 많은 사람들이 다양한 경험을 해나가며 의견을 주고받아온 결과이기 때문에, 자신의 애플리케이션에 적합한 좋은 패턴만 잘 따라가더라도 충분히 안정적이고 재사용성이 뛰어난 프로그램을 만들 수 있을 것이다.
    • 디자인 패턴은 어떠한 문제를 해결하냐에 따라 생성 패턴, 구조 패턴, 행위 패턴으로 구분된다.
      • 생성 패턴은 주어진 상황에 적합한 객체를 생성하는 방법에 중점을 둔다.
      • 구조 패턴은 객체의 구성과 각 객체 간의 관계를 인식하는 방법에 중점을 둔다.
      • 행위 패턴은 시스템 내의 객체 간 커뮤니케이션을 개선하거나 간소화하는 방법에 중점을 둔다.
    • 생성 패턴에는 다음이 있다.
      • 생성자 패턴: 자바스크립트는 생성자 함수 또는 클래스를 통해 객체를 생성할 수 있다. 클래스와 생성자 함수는 문법에 있어 약간의 차이가 있지만, 클래스 또한 프로토타입 체인을 통해 상속을 구현하며, 클래스의 주요 기능을 생성자 함수를 통해 대부분 흉내 내는 것이 가능하다. 클래스 내부에도 객체를 초기화하는 생성자(constructor)가 있고, 클래스 본문에 인스턴스의 메서드를 생성할 수 있다. 단, 클래스에 바로 메서드를 정의하면 모든 인스턴스를 만들 때 새로운 메서드 함수를 다시 만들어서 비효율이 발생한다. 이런 경우 {생성자}.prototype 객체에 메서드를 추가해 주면 생성되는 모든 인스턴스가 하나의 함수를 참조할 수 있게 된다.
      • 모듈 패턴: 기존엔 자바스크립트에 모듈이 없었지만, ES6 이후로 모듈 시스템을 사용하는 것이 가능해졌다. 모듈을 이용하면 외부에 공개할 값-메서드와 비공개 처리할 값-메서드를 구분하여 사용할 수 있다는 점에서 유용하다. 값의 공개-비공개 처리에는 클로저가 주로 사용된다. 모듈 패턴을 사용하면 쉽고 직관적이다. 또, 원하는 값을 비공개 처리할 수 있어 외부의 접근으로 변경되면 안 되는 값을 안전하게 처리할 수 있고(클래스의 this를 키로 하는 Weakmap을 사용하면 좀 더 안전하게 값을 관리할 수도 있다), 모듈 간 스코프 구분으로 네임 스페이스의 중복을 방지해 준다는 강력한 장점이 있다.
      • 노출 모듈 패턴: '공개 변수나 메서드에 접근하기 위해 가져온 메인 객체의 이름을 반복해서 사용해야 한다는 점에 답답함을 느끼면서 생겨났다'라고 한다. 노출 모듈 패턴은 모듈에서 공개할 값과 함수(메서드)들을 myRevealingModule과 같은 이름의 객체로 래핑 해서 내보낸다. 래핑할 때의 프로퍼티 이름은 외부에서 사용할 직관적이고 쉬운 이름을 사용하도록 할 수 있다. 노출 모듈 패턴은 코드의 일관성을 유지할 수 있고, 외부에 공개하는 객체를 알아보기 쉽게 개선하여 가독성도 높일 수 있다는 장점이 있다. 하지만, 공개 함수를 래핑해서 내보내기 때문에 함수의 수정이 까다롭다는 단점이 있다고 한다(솔직히 무슨 얘기인지 잘 와닿지 않는다).
      • 싱글톤 패턴: 특정 클래스의 인스턴스를 1개만 생성하는 것으로 보장하는 디자인 패턴이다. 생성자가 여러 번 호출되더라도 기존에 만들어 둔 인스턴스가 반복적으로 재활용되고, 새로운 인스턴스를 만들지 않는다. 이렇게 하면 메모리 사용에 이점이 있고, 전역에서의 상태를 공유하여 참조하는 모든 곳의 데이터 일관성을 유지시킬 수 있다. 하지만, 싱글톤 패턴을 많이 사용하면 유지 보수가 어렵고, 테스트가 힘들며, 사이드 이펙트의 영향이 크다는 단점이 있다(하나의 인스턴스를 많은 곳에서 참조하기 때문에, 하나가 바뀌면 많은 곳에 영향이 간다). 무엇보다도, 자바스크립트는 클래스를 지정해야만 객체를 생성할 수 있는 일부 다른 클래스 기반 객체 지향 언어와 다르게, 객체를 리터럴로 생성할 수 있다. 따라서, 1개의 인스턴스를 만들기 위한 싱글톤이 아니라 일반 객체를 만들면 되기 때문에, 자바스크립트에서의 싱글톤이 꼭 필요한지에 대한 고민이 필요하다.
      • 프로토타입 패턴: 이미 존재하는 객체를 복제해 만든 템플릿을 기반으로 새 객체를 생성하는 패턴이다. 자바스크립트만이 가진 고유한 방식으로 작업을 가능해주는 강력한 기능이다. 프로토타입을 사용하면 상속받은 모든 인스턴스가 메서드의 참조를 같게 하여 메모리 효율상 월등히 우수하다는 장점이 있다.
      • 팩토리 패턴: 객체를 생성해 주는 생성자를 묶어서 객체를 생성해주는 '공장'을 만드는 패턴이다. 객체를 만들어주는 팩토리를 목표를 기준으로 캡슐화해주면 추상 팩토리 패턴으로 확장도 가능하다. 객체 또는 컴포넌트의 생성 과정이 높은 복잡성을 갖거나, 상황에 따라 많은 객체 인스턴스를 편리하게 만들어야 하는 상황에서 유용하지만, 잘못 사용하면 애플리케이션의 복잡도가 크게 증가할 수 있다. 프로그래밍을 할 때 객체를 생성하는 것이 설계의 중요한 목표가 되는 경우에 한해서 활용하면 좋다.

'TIL' 카테고리의 다른 글

241130 TIL  (1) 2024.12.01
241129 TIL  (2) 2024.11.29
241127 TIL  (1) 2024.11.27
241126 TIL  (0) 2024.11.27
241125 TIL  (0) 2024.11.26