쾌락코딩

Typescript Quickstart 공부5. 고급 타입

|

타입 가드

타입을 유니언 타입(|)으로 복수개를 받을 경우, 유니언 타입에 대한 타입 검사를 손수 해서 안전성을 주는 방법을 타입 가드라고 한다. typedof로 확인해서 타입에 알맞는 처리를 함.

문자열 리터럴 타입

정의한 문자열만 할당받을 수 있게 하는 타입.

let event: "keyup" = "keyup"; // 가능
let event: "keyup" = "keyup2"; // 불가능

룩업 타입(인덱스 접근 타입)

keyof 키워드를 통해 타입 T의 하위 타입을 생성해 낸다.

interface Profile {
  name: string;
  gender: string;
  age: number;
}

// 위의 인스턴스를 keyof를 이용해 룩업 타입으로 선언
type Profile1 = keyof Profile;

// 이제 Profile1 타입은 Profile의 속성인 name, gender, age 중 하나를 할당받을 수 있는 하위 타입이 되었다.
let pVlaue1: Profile1 = "name";

non-nullable 타입

컴파일러가 null 이나 undefined를 엄격하게 제한한다. 타입스크립트 2.0 전에는 null이나 undefined를 모든 타입의 변수에 할당할 수 있었다. 그러나 이것은 타입에 모호성을 준다. 그래서 이 것을 막기위해 나온 것이다.

이 타입은 :string 이나 :number 처럼 :non-nullable 이렇게 하는게 아니다. 설정을 해줘야 한다. tsconfig.json 파일의 strictNullChecks 옵션을 true로 설정하면 된다. 이렇게 되면 string이든 number든 boolean이든 null이나 undefined를 할당할 수 없다.

이렇게 설정한 후에도 null이나 undefined를 예외적으로 할당하고 싶을 때가 있을 것이다. 그럴땐 방법이 있다. 유니언 타입으로 만드는 것이다.

let title: string | null;
let title2: string | undefined;

this 타입

인터페이스와 클래스의 하위 타입이면서 이들을 참조할 수도 있는 타입. 클래스 멤버 변수나 생성자에서 this 타입을 사용하면 가장 가까운 클래스의 인스턴스를 참조한다.

플루언트 패턴을 구현할 때 this 타입 반환을 선언하고 자신을 가리키는 this를 반환한다.

class AddCalc {
  public constructor(public value: number = 0) {}
  public add(operand: number): this {
    this.value += operand;
    return this;
  }
}

class MyCalc extends AddCalc {
  public multiply(operand: number): this {
    this.value *= operand;
    return this;
  }
}

let calc = new MyCalc(3).multiply(5).add(10); // 3*5=100
console.log(calc.value); // 25

정리

위의 타입들은 내가 타입스크립트를 사용하면서 거의 보지 못한 타입들이다. 타입스크립트를 사용하지 얼마 되지 않아서 그럴수도 있고, 내가 몰랐던 거라 보고도 지나친 것일수 도 있지만 두고두고 볼 필요는 있는 것 같다.

react에서 scroll down 구현

|

리액트에서 스크롤 다운을 구현하기위해 열심히 구글링을 하다가, 몇가지 기록할만한 것들을 발견했다.

element.scrollIntoView

스크롤 다운 버튼을 눌렀을 때, 보여지고 싶은 element에서 scrollIntoView메서드를 호출하면 된다. 이 메서드에는 몇가지 options을 줄 수 있다. options는 그리 어렵지도않고 다양하지도 않아서 간단히 MDN 링크로만.

element 참조를 위한 ref

react에서는 해당 element를 ref를 사용해 참조하면 된다. 문제는 typescript에서 ref를 사용하여 참조하려니까 조금 까다로운 점이 있었다(그래서 사실 내가 구현한 방법도 좋은 방법은 아닌 것 같지만 좋은 방안을 찾으면 바꿔야겠다.)

만약 react 16.3+ 버전을 사용한다면 React.createRef() 간단하게 참조할 수 있다고 한다.

class TestApp extends React.Component<AppProps, AppState> {
    private stepInput: React.RefObject<HTMLInputElement>;
    constructor(props) {
        super(props);
        this.stepInput = React.createRef();
    }
    render() {
        return <input type="text" ref={this.stepInput} />;
    }
}

위의 코드와 같이 HTMLInputElement타입 뿐만 아니라 HTMLDivElement와 같은 대부분의 요소 타입이 존재한다. 만약 div태그를 참조하고 싶다면 HTMLDivElement태그를 사용하면 된다.

그러나 나는 같은 아래와 같은 상황에서 IntroTwo를 참조하여 스크롤 다운 버튼을 눌렀을 때 바로 IntroTwo부분을 보여주고 싶었다.

import IntroTwo from '../component/Main/IntroTwo';

class IntroContainer extends React.Component<IProps, IState> {
  private introTwoLocationRef: React.RefObject<IntroTwo>;

  constructor (props: any) {
    super(props);
    this.introTwoLocationRef = React.createRef();
  }

  onClickScrollDownButton = () => {
    console.log(this.introTwoLocationRef)
  };

  render () {
    return (
      <React.Fragment>
        <IntroOne onClickScrollDownButton={this.onClickScrollDownButton} />
        <IntroTwo ref={this.introTwoLocationRef}/> // 요거
      </React.Fragment>
    );
  }
}

위와 같은 코드를 작성하면 될 줄 알았는데, onClickScrollDownButton으로 찍힌 콘솔을 보면 div나 input을 참조하고 콘솔을 찍었을 때 나오는 것들이 하나도 나오지 않았다. 에러가 난건 아니지만 element가 아닌 클래스라 그런지 내가 원했던 속성들이 없었고 따라서 scrollIntoView을 사용할 수가 없었다(생각해보면 introTwoLocationRef은 엘리먼트가아니라 클래스이기 때문에 scrollTop값은 속성이 없는게 당연한 듯). 그래서 우선은 임시방편(?)으로 아래와 같은 코드를 작성해 원하는 기능을 구현하긴 했다.

import IntroTwo from '../component/Main/IntroTwo';

class IntroContainer extends React.Component<IProps, IState> {
  private introTwoLocationRef: React.RefObject<HTMLDivElement>;

  constructor (props: any) {
    super(props);
    this.introTwoLocationRef = React.createRef();
  }

  onClickScrollDownButton = () => {
    const introTwoLocation: HTMLDivElement = this.introTwoLocationRef
      .current as HTMLDivElement;
    introTwoLocation.scrollIntoView({
      behavior: 'smooth',
      block: 'start'
    });
  };

  render () {
    return (
      <React.Fragment>
        <IntroOne onClickScrollDownButton={this.onClickScrollDownButton} />
        <div style= ref={this.introTwoLocationRef} />
        <IntroTwo />
      </React.Fragment>
    );
  }
}

중간에 임의의 div 요소를 넣고 height을 0px로 주면, div와 IntroTwo의 scrollTop은 같은 위치를 가르키기 때문에 div요소에서 scrollIntoView을 호출하면 IntroTwo가 보여진다.

정리

이렇게 구현하는게 뭔가 좋지는 않은…찜찜한 느낌이 든다. 자식 컴포넌트가 랜더링하는 많은 element중 하나를 참조할 수 있는 방법을 찾아보고 적용해봐야겠다. 분명히 있긴한것 같은데 typescript라 그런지 자료가 많지는 않은 것 같다.

Typescript Quickstart 공부4. 모듈

|

네임스페이스

네임스페이스는 하나의 독립된 이름 공간을 만들고 여러 파일에 걸쳐 하나의 이름 공간을 공유할 수 있다. 같은 네임스페이스의 이름 공간이라면 파일 B가 파일 A에 선언된 모듈을 참조할 수 있는데, 참조할 때는 별도의 참조문을 선언하지 않아도 된다. 이 말은 곧, 파일이 다르더라도 같은 네임스페이스 안에 속한다면 변수든, 함수든, 클래스든 이름이 중복되어선 안된다는 말이다. 즉, 네임스페이스(내부모듈)은 여러개의 파일을 마치 하나의 파일인 것 처럼 사용할 수 있게 하는 것이다.

선언하는 방식

namespace Hello { ... }

한 파일에 여러 네임스페이스를 선언할 수 있다. 이럴 경우, 같은 파일이지만 export된 모듈들만 불러올 수 있다.

namespace MyInfo1 {
  export let name = "happy1";
  export function getName2() {
    return MyInfo2.name2;
  }
}

namespace MyInfo2 {
  export let name2 = "happy2";
  export function getName() {
    return MyInfo1.name;
  }
}

console.log(MyInfo.getName2()); // happy2
console.log(MyInfo2.getName()); // happy1

네임스페이스 모듈

네임스페이스는 export를 이용해 모듈로 선언할 수 있다.

export namespace Car { ... }

그런 다음 import문으로 불러올 수 있다.

import * as ns from "./car1";

외부 모듈

원래 알던 내용들이고, 딱히 타입스크립트 만의 어떤 내용이 있지 않은것 같다. 몇가지 내가 몰랐던 점만 적자.

디폴드 모듈 선언 2가지 방법

// 기존에 늘 하던 방식
export default {
  title: "hello world!",
  length: 11
};
// 변형된 형태
export a = {
  title: "hello world!",
  length: 11
};
export { a as default };

당연한 이야기지만 default 키워드는 한 번만 사용해야 하므로 아래와 같은 코드는 불가능하다.

export { p as default, h as default };

끝내며

이번 파트도 역시 typescript를 위한 파트라기 보다는 기본적인 모듈을 이해하기 위한 파트였던 것 같다. 딱히 typescript만의 특별한 점은 크게 없는 것 같다. typescript는 es6 모듈 형식이 default라는 것 정도…

Typescript Quickstart 공부3. 클래스와 인터페이스

|

클래스

클래스에서의 접근제한자

es6에서는 private, public, protected와 같은 접근 제한자를 제공하지 않는다. 반면 타입스크립트는 지원하기 때문에 더 완벽한 객체지향 코딩이 가능하다. 생성자 매개변수에 접근 제한자를 추가하면 코드가 깔끔해진다고 한다.

class Cube {
  public width: number;
  private length: number;
  protected heigth: number;
  constructor(pWidth: number, pLength: number, pHeight: number) {
    this.width = pWidth;
    this.length = pLength;
    this.heigh = pHeight;
  }
}

위 코드는 복잡하다고한다(나는 자바 프로그래밍을 해본 경험이 있어서 그런지 개인적으로 이게 가독성이 더 좋다고 생각한다). 아래와 같은 방법으로 더 짧은 코드작성이 가능하다.

class Cube {
  constructor(public width: number, private length: number, protected height: number) {}
}

생성자 매개변수에 접근 제한자를 추가한 것만으로도 생성자 매개변수가 클래스의 멤버 변수가 되는 효과가 있다.

기본 접근 제한자

private, public, protected와 함께 default 접근 제한자가 존재한다. 이 것은 접근 제한자 선언을 생략할 때 자동으로 적용된다. 기본 접근 제한자로 사용하면 접근성은 어느정도가 될까? 기본 접근 제한자는 대체로 public이다. 그러나 예외적인 부분이 있는데, 생성자 매개변수에 접근제한자가 생략되면 생성자 내부에서만 접근할 수 있게 된다. 아주 당연한 이야기이다. 생성자 파라미터 부분에 접근제한자가 없다면, 그 것은 그냥 일반적인 함수의 변수 스코프와 다를게 없기 때문이다. 그러나 접근 제한자나 readonly가 붙으면 클래스의 매개변수 속성이 돼 멤버 변수가 된다.

추상 클래스

추상 클래스(abstract class) 역시 지원된다.

abstract class 추상클래스 {
  abstract 추상메서드();
  abstract 추상멤버변수: string;
  public 구현메서드(): void {
    // 공통적으로 사용할 로직을 추가함
    // 로직에서 필요 시 추상 메서드를 호출해 구현 클래스의 메서드가 호출되게 함
    this.추상메서드();
  }
}

abstract 키워드는 static이나 private와 함께 선언할 수 없다. public이나 protected는 가능한데, 자식 클래스에서 추상 메서드를 오버라이딩 해야된다는 것을 생각하면 당연한 이야기다.

인터페이스

인터페이스는 타입이며 컴파일 후에는 사라진다(es6에는 인터페이스가 없다). 따라서 typeof을 이용해서 인터페이스의 타입을 조사할 수 없다.

interface Car {
  // 접근 제한자는 설정할 수 없다.
  speed: number;
}

자식 인터페이스는 extends 키워드를 이용해 부모 인터페이스를 확장할 수 있다.

interface 자식인터페이스이름 extends Car {}

인터페이스의 역할

js객체는 구조를 고정할 수 없고 쉽게 변화하는 특성이 있다. 객체는 유지보수와 확장, 안전성을 고려해 선언과 동시에 고정할 필요가 있다. 인터페이스를 이용해 객체의 구조를 고정할 수 있다.

객체 리터럴을 타입으로 사용하기

// type 정의
type objectLiteralType =  { name: string, city: string};
// 또는 아래도 가능
// interface Person {
//   name: string;
//   city: string; 
// }

let person: objectLiteralType[];

person = [
  { name: "a", city: "seoul"},
  { name: "b", city: "seoul"}
]

readonly 제한자

readonly는 타입스크립트 2.0 부터 지원된다. readonly가 선언된 변수는 초기화 되면 절대 재할당이 불가능하다.

interface ICount {
  readonly count: number;
}

class TestReadonly implements ICount {
  readonly count: number;
  getCount() {
    // 아래는 에러가 난다. readonly는 메서드나 함수안에서 선언할 수 없다
    // readonly count2:number = 0;
    // 반면 const는 가능하다.
    const count2:number = 0;
  }
}

const도 재할당이 불가능한건 마찬가지인데 그렇다면 이 둘은 무슨 차이일까? 스택오버플로우에 따르면 const 변수에 재할당이 되는지 안되는지, 그래서 에러를 뿜어 낼지 말지는 오로지 runtime때 알 수 있다. 하지만 readonly를 사용한다면? 컴파일 시간에 알 수 있다. 실수로 값을 재 할당 하려고 하는 순간 빨간줄이 그인다!

나머지

클래스와 인터페이스 파트는 전체적으로 분량이 상당히 많지만, 타입스크립트와 관련된 내용이라기 보다는 기본적인 OOP 프로그래밍에 대한 설명, ES6에 대한 설명이 대부분이라서 딱히 적을 부분이 없다.

Typescript Quickstart 공부2. 함수

|

기본 형태

function pow( x: number, y: number = 2 ): number {
  // bla bla
}

나머지 매개변수(rest parameter)에 타입 지정

rest parameter는 개수가 정해지지 않은 인수를 배열로 받을 수 있는 기능이다. 따라서 타입을 배열로 받아야 한다.

const concat = (a: string, ...restParameter: string[]) => {
  return a + " " + restParameter.json(" ");
}

객체 리너럴의 선언과 객체 리터럴 타입의 선언

일반적으로 객체 리터럴 선언은 아래와 같이 한다.

let person = {
  name: 'elecoder',
  hello: function (name2: string) {
    console.log("hello, " + this.name +" " + name2);
  }
};

console.log("World"); // hello, elecoder World

객체 리터럴의 hello 속성에 선언된 함수는 내부의 로컬 스코프에서 다른 객체 속성에 접근하려 할 때 코드 어시스트가 동작 하지 않는다(설명에는 나와 있지 않지만 내 생각에는 this가 어디에 바인딩 될지 모르기 때문이지 않을까?). 이런 상황에서 타입스크립트를 쓰는 장점 중에 하나인 코드 어시스트를 받기 위해서는, this에도 타입을 정해주면 된다. 객체 리터럴의 타입은 인터페이스를 이용해 정의한다.

// this는 반드시 첫 번째 매개변수로 선언해야한다.
// 컴파일되면 없어지는 매개변수이다.
interface PersonType {
  name: string;
  hello(this: PersonType, name2: string): string;
}

let person = {
  name: 'elecoder',
  hello: function (name2: string) {
    console.log("hello, " + this.name +" " + name2);
  }
};

console.log("World"); // hello, elecoder World

이제 코드 어시스트가 발동한다.(생각보다 거의 모든게 타입 지원이 가능하구나…)

익명 함수 할당시 팁

익명 함수의 매개변수나 반환값에 타입을 일일히 지정해 줄 수 있지만, 익명 함수가 구현체이므로 타입을 선언하면 형태가 복잡해진다. 따라서 익명 함수에 선언된 타입을 별도로 type 엘리어스로 분리하여 선언하고, 필요할 때 타입을 사용하면 좋다.

// reuse 가능
type calcType = (a: number, b: number) => number;

let addCalc: calcType = (a, b) => a + b;
let minusCalc: calcType = (a, b) => a - b;