0228 - 0306

0228

잉여 속성 체크의 한계 인지하기

타입이 명시된 변수에 객체 리터럴을 할당하거나 함수에 매개변수로 전달할 때, 타입스크립트는 해당 타입의 속성이 있는지, 그리고 그 외의 속성은 없는지를 확인한다.

interface Room {
  numDoors: number;
  ceilingHeightFt: number;
}

const r: Room = {
  numDoors: 1,
  ceilingHeightFt: 10,
  elephant: "present",
  // 오류 - 'Room' 형식에 'elephant'가 없습니다.
};
  • 구조적 타이핑 관점으로 생각해보면 오류가 발생하지 않아야 한다.
  • 임시 변수 obj를 선언하고 Room 타입에 할당하는 것은 가능하다.
const obj = {
  numDoors: 1,
  ceilingHeightFt: 10,
  elephant: "present",
};

const r: Room = obj;
// 정상
  • obj 타입은 Room 타입의 부분 집합을 포함하므로, Room에 할당 가능하며 타입 체커도 통과한다.
  • 첫 번째 예제에서는, 구조적 타입 시스템에서 발생할 수 있는 중요한 종류의 오류를 잡을 수 있도록 ‘잉여 속성 체크’라는 과정이 수행되었다.
  • 그러나 잉여 속성 체크 역시 조건에 따라 동작하지 않는다는 한계가 있고, 통상적인 할당 가능 검사와 함께 쓰이면 구조적 타이핑이 무엇인지 혼란스러워질 수 있다.
  • 잉여 속성 체크가 할당 가능 검사와는 별도의 과정이라는 것을 알아야 타입스크립트 타입 시스템에 대한 개념을 정확히 잡을 수 있다.

의도와 다르게 작성된 코드

interface Options {
  title: string;
  darkMode?: boolean;
}

function createWindow(options: Options) {
  if (options.darkMode) {
    setDarkMode();
  }
}

createWindow({
  title: "Spider Solitaire",
  darkmode: true,
  // 'Options' 형식에 'darkmode'가 없습니다.
});

위와 같은 코드를 실행하면 런타임에 어떠한 종류의 오류도 발생하지 않는다.
그러나 타입스크립트가 알려주는 오류 메시지처럼 의도한 대로 동작하지 않을 수 있다.
Options 타입은 범위가 매우 넓기 때문에, 순수한 구조적 타입 체커는 이런 종류의 오류를 찾아내지 못한다.
왜냐하면 타입스크립트 타입 시스템은 구조적 타이핑을 따르기에 string 타입인 title 속성과 ‘또 다른 어떤 속성’을 가지는 모든 객체는 Options 타입의 범위에 속하기 때문이다.

잉여 속성 체크

잉여 속성 체크를 이용하면 기본적으로 타입 시스템의 구조적 본질을 해치지 않으면서도 객체 리터럴에 알 수 없는 속성을 허용하지 않음으로써, 문제점을 방지할 수 있다.

const intermediate = { darkmode: true, title: "Ski Free" };
const o: Options = intermediate; // 정상

다만 위의 예시에서 두 번째 줄은 객체 리터럴이 아니기에 잉여 속성 체크가 적용되지 않고 오류는 사라진다.

잉여 속성 체크는 타입 단언문을 사용할 때도 적용되지 않는다.
단언문보다 선언문을 사용해야 하는 이유 중 하나이다.

잉여 속성 체크를 원치 않는다면, 인덱스 시그니처를 사용해서 타입스크립트가 추가적인 속성을 예상하도록 할 수 있다.

interface Options {
  darkMode?: boolean;
  [otherOptions: string]: unknown;
}
const o: Options = { darkmode: true }; // 정상

선택적 속성만 가지는 약한 타입에도 비슷한 체크가 동작한다.

interface LineChartOptions {
  logscale?: boolean;
  invertedYAxis?: boolean;
  areaChart?: boolean;
}

const opts = { logScale: true };
const o: LineChartOptions = opts;
// '{ logScale: boolean; }' 유형에 'LineChartOptions' 유형과 공통적인 속성이 없습니다.
  • 구조적 관점에서 LineChartOptions 타입은 모든 속성이 선택적이므로 모든 객체를 포함할 수 있다.
  • 이런 약한 타입에 대해서 타입스크립트는 값 타입과 선언 타입에 공통된 속성이 있는지 확인하는 별도의 체크를 수행한다.
  • 그러나 잉여 속성 체크와 다르게 약한 타입과 관련된 할당문마다 수행되어, 임시 변수를 제거하더라도 공통 속성 체크는 여전히 동작한다.
  • 잉여 속성 체크는 선택적 필드를 포함하는 Options 같은 타입에 특히 유용한 반면, 적용 범위도 매우 제한적이며 오직 객체 리터럴에만 적용된다.

함수 표현식에 타입 적용

타입스크립트에서는 함수 표현식을 사용하는 것이 좋다. 함수 매개변수부터 반환값까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용할 수 있다는 장점이 있기 때문이다.

함수 타입의 장점

함수 타입의 선언은 불필요한 코드의 반복을 줄인다.

예를 들어, 사칙연산을 하는 함수를 작성할 때 반복되는 함수 시그니처를 하나의 함수 타입으로 통합할 수도 있다.

type BinaryFn = (a: number, b: number) => number;
const add: BinaryFn = (a, b) => a + b;
...
  • 만약 라이브러리를 직접 만드는 경우가 있다면, 공통 콜백 함수를 위한 타입 선언을 제공하는 것이 좋다.

시그니처가 일치하는 다른 함수가 있을 때도 함수 표현식에 타입을 적용해 볼 만하다.
예를 들어, fetch 함수는 특정 리소스에 HTTP 요청을 보내고, response.json()를 사용해 응답의 데이터를 추출한다.
fetch 함수의 경우 ‘404 Not Found’가 발생했을 때 reject 하지 않기 때문에 상태 체크를 수행해 줄 checkedFetch 함수를 선언문과 표현식 두 가지 방식으로 작성해보자

async function checkedFetch(input: RequestInfo, init?: RequestInit) {
  const response = await fetch(input, init);

  if (!response.ok) {
    throw new Error("Request failed: " + response.status);
  }
  return response;
}

const checkedFetch: typeof fetch = async (input, init) => {
  const response = await fetch(input, init);

  if (!response.ok) {
    throw new Error("Request failed: " + response.status);
  }
  return response;
};
  • 함수 표현식과 기존의 타입을 typeof로 적용한 경우 타입스크립트가 매개변수 타입을 추론할 수 있게 해 준다.
  • 표현식 타입 구문은 checkedFetch의 반환 타입을 보장하기에 throw 대신 return을 사용했다면, 타입스크립트는 그 실수를 잡아낸다.
  • 선언문으로 작성한 경우에도 return을 사용하면 오류가 발생하지만, 구현체가 아닌 함수를 호출한 위치에서 오류가 발생한다.
  • 함수의 매개변수에 타입 선언을 하는 것보다 함수 표현식 전체 타입을 정의하는 것이 코드도 간결하고 안전하다.
  • 다른 함수의 시그니처를 참조하려면 typeof fn을 사용하면 된다.

타입과 인터페이스의 차이점 알기

타입스크립트에서 타입을 정의하는 방법은 타입과 인터페이스 두 가지 방법이 존재한다.
대부분의 경우에는 타입을 사용해도 되고, 인터페이스를 사용해도 된다.
그러나 타입과 인터페이스 사이에 존재하는 차이를 분명하게 알고, 같은 상황에서는 동일한 방법으로 명명된 타입을 정의해 일관성을 유지해야 한다.

비슷한 점

  • 명명된 타입은 인터페이스로 정의하든 타입으로 정의하는 상태에는 차이가 없다.
    • 예를 들어, 객체 리터럴로 추가 속성과 함께 할당한다면 동일한 오류가 발생한다.
  • 인덱스 시그니처는 인터페이스와 타입에서 모두 사용 가능하다.
  • 함수 타입도 인터페이스와 타입에서 모두 사용 가능하다.
  • 타입 별칭과 인터페이스는 모두 제너릭이 가능하다.
  • 인터페이스는 타입을 확장할 수 있고, 타입은 인터페이스를 확장할 수 있다.
interface IStateWithPop extends TState {
  population: number;
}
type TStateWithPop = IState & { population: number };
* IStateWithPop과 TStateWithPop는 동일하다.
* 여기서 주의할 점은 인터페이스는 유니온 타입 같은 복잡한 타입을 확장하지는 못한다는 것이다.
* 복잡한 타입을 확장하고 싶다면 타입과 &를 사용해야 한다.
  • 클래스를 구현(implements)할 때는, 타입과 인터페이스를 둘 다 사용할 수 있다.

다른 점

  • 인터페이스는 타입을 확장할 수 있지만, 유니온은 할 수 없다.
  • 튜플과 배열 타입도 type 키워드를 이용해 더 간결하게 표현할 수 있다.
type Pair = [number, number];
type StringList = string[];

// 인터페이스로 구현 시
interface Tuple {
  0: number;
  1: number;
  length: 2;
}
  • 그러나 인터페이스로 튜플과 비슷하게 구현하면 튜플에서 사용할 수 있는 concat 같은 메서드들을 사용할 수 없기에 type 키워드로 구현하는 것이 낫다.
  • 인터페이스는 타입에 없는 기능이 있는데, 그 중 하나는 보강(augment)이 가능하다는 것이다.
interface IState {
  name: string;
  capital: string;
}

interface IState {
  population: number;
}

const wyoming: IState = {
  name: "Wyoming",
  capital: "Cheyenne",
  population: 500,
};
  • 이처럼 속성을 확장하는 것을 선언 병합이라고 한다. 선언 병합은 주로 타입 선언 파일에서 사용된다.
  • 타입스크립트는 여러 버전의 자바스크립트 표준 라이브러리에서 여러 타입을 모아 병합한다.
    • 예를 들어, Array 인터페이스는 lib.es5.d.ts에 정의되어 있고 기본적으로 이에 선언된 인터페이스가 사용된다.
    • 그러나 tsconfig.json의 lib 목록에 ES2015를 추가하면 타입스크립트는 이에 선언된 인터페이스를 병합한다.
    • 결과적으로 각 선언이 병합되어 전체 메서드를 가지는 하나의 Array 타입을 얻게 된다.

결론

  • 복잡한 타입이라면 타입 별칭을 사용
  • 간단한 객체 타입이라면 일관성과 보강의 관점에서 고려해 볼 것
    • 일관되게 코드베이스에서 작업 중인 문법을 사용
    • 아직 스타일이 확립되지 않은 프로젝트라면 향후 보강 가능성을 고려해 볼 것
    • 어떤 API에 대한 타입 선언을 작성해야 한다면 인터페이스를 사용하는 것이 좋음
    • API가 변경될 때 사용자가 인터페이스를 통해 새로운 필드를 병합 할 수 있어 유용하기 때문
  • 프로젝트 내부적으로 사용되는 타입에 선언 병합이 발생하는 것은 잘못된 설계




0301

타입 연산과 제너릭 사용으로 반복 줄이기

DRY 원칙 don’t repeat yourself - 같은 코드를 반복하지 말라는 원칙

타입 중복은 코드 중복만큼 많은 문제를 발생시킨다.
타입 간에 매핑하는 방법을 익히면, 타입 정의에서도 DRY의 장점을 적용할 수 있다.

  • 반복을 줄이는 가장 간단한 방법으로는 타입에 이름을 붙이는 것이다.
  • extends를 사용해서 인터페이스의 반복을 피할 수 있다.

중복 제거 예제

전체 애플리케이션의 상태를 표현하는 State 타입과 단지 부분만 표현하는 TopNavState가 있는 경우

interface State {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
  pageContents: string;
}

interface TopNavState {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
}

TopNavState를 확장하여 State를 구성하기보다, State의 부분 집합으로 TopNavState를 정의하는 것이 바람직하다.

State 인덱싱

State를 인덱싱 하여 속성의 타입에서 중복을 제거할 수 있다.

interface TopNavState {
  userId: State["userId"];
  pageTitle: State["pageTitle"];
  recentFiles: State["recentFiles"];
}

이렇게 구현하면 State 내의 타입이 바뀌어도 TopNavState에도 반영된다.

맵드 타입

반복되는 코드를 더 줄이기 위해 맵드 타입을 사용하면 좀 더 나아진다.

type TopNavState = {
  [k in "userId" | "pageTitle" | "recentFiles"]: State[k];
};

맵드 타입은 배열의 필드를 루프 도는 것과 같은 방식이다.

Pick

이 패턴은 표준 라이브러리에서도 찾을 수 있으며, Pick이라고 한다.

type TopNavState = Pick<State, "userId" | "pageTitle" | "recentFiles">;

제네릭 타입으로 함수에서 매개변수를 받아 결괏값을 반환하는 것처럼 T와 K 두 가지 타입을 받아서 결과 타입을 반환한다.

태그된 유니온

interface SaveAction {
  type: "save";
}

interface LoadAction {
  type: "load";
}

type Action = SaveAction | LoadAction;
type ActionType = Action["type"];

Action 유니온을 인덱싱하면 타입 반복 없이 ActionType을 정의할 수 있다.

keyof

생성하고 난 다음 업데이트가 되는 클래스를 정의한다면, update 메서드 매개변수의 타입은 생성자와 동일한 매개변수이면서, 타입 대부분이 선택적 필드가 된다.
이때 매핑된 타입과 keyof를 사용하면 모든 타입을 선택적 필드로 만들 수 있다.

type OptionsUpdate = {
  [k in keyof Options]?: Options[k];
};

표준 라이브러리에 Partial이라는 이름으로 포함되어 있기도 하다.

typeof

값의 형태에 해당하는 타입을 정의하고 싶다면 typeof를 사용하면 된다.

const INIT_OPTIONS = {
  width: 640,
  height: 480,
  color: "#00FF00",
  label: "VGA",
};

type Options = typeof INIT_OPTIONS;
  • 자바스크립트의 런타임 연산자 typeof를 사용한 것처럼 보이지만, 실제로는 타입스크립트 단계에서 연산되며 훨씬 더 정확하게 타입을 표현한다.
  • 값으로부터 타입을 만들어 낼 때는 선언의 순서에 주의해야 한다.
    • 타입 정의를 먼저하고 값이 그 타입에 할당 가능하다고 선언하는 것이 좋다.
    • 이렇게 해야 타입이 더 명확해지고, 예상하기 어려운 타입 변동을 방지할 수 있다.

ReturnType

함수나 메서드의 반환 값에 명명된 타입을 만들고 싶은 경우 ReturnType 제너릭을 사용하면 된다.

type UserInfo = ReturnType<typeof getUserInfo>;

함수의 타입을 가져와서 리턴 값의 타입을 반환한다.

제너릭 타입 매개변수 제한

제너릭 타입에서 매개변수를 제한할 수 있는 방법은 extends를 사용하는 것이다.
extends를 이용하면 제너릭 매개변수가 특정 타입을 확장한다고 선언할 수 있다.

interface Name {
  first: string;
  last: string;
}
type DancingDuo<T extends Name> = [T, T];

const couple1: DancingDuo<Name> = [
  { first: "Fred", last: "Astaire" },
  { first: "Jeremy", last: "Kim" },
]; // OK
  • 현재 타입스크립트에서는 선언부에 항상 제너릭 매개변수를 작성하도록 되어 있다.
  • 타입스크립트가 제너릭 매개변수의 타입을 추론하게 하기 위해, 함수를 작성할 때는 신중하게 타입을 고려해야 한다.
const dancingDuo = <T extends Name>(x: DancingDuo<T>) => x;

extends keyof

Pick의 정의는 extends를 사용해서 완성할 수 있다.

type Pick<T, K extends keyof T> = {
  [k in K]: T[k];
};
  • K의 범위를 설정하지 않으면 너무 넓고, 인덱스로 사용될 수 있는 string | number | symbol에 속해야 한다.
  • K는 실제로는 T의 키의 부분집합이 되어야 한다.

동적 데이터에 인덱스 시그니처 사용

타입스크립트에서는 타입에 인덱스 시그니처를 명시하여 유연하게 매핑을 표현할 수 있다.

type Rocket = {
  [property: string]: string;
};

const rocket: Rocket = {
  name: "Falcon 9",
  variant: "v1.0",
  thrust: "4,940 kN",
};
  • [property: string]: string이 인덱스 시그니처이며, 다음 세 가지 의미를 담고 있다.
  • 키의 이름 - 키의 위치만 표시하는 용도로 타입 체커에서는 사용하지 않는다.
  • 키의 타입 - string이나 number 또는 symbol의 조합이어야 하지만, 보통은 string을 사용한다.
  • 값의 타입 - 어떤 것이든 될 수 있다.

인덱스 시그니처 타입 체크 단점

  • 잘못된 키를 포함해 모든 키를 허용한다.
  • 특정 키가 필요하지 않다. {}도 유효한 Rocket 타입이다.
  • 키마다 다른 타입을 가질 수 없다.
  • 키는 무엇이든 가능하기 때문에 자동 완성 기능 같은 언어 서비스를 받을 수 없다.

-> 인덱스 시그니처는 부정확하므로 더 나은 방법을 찾아야 한다. 즉, 가능하다면 인터페이스, Record, 매핑된 타입 같은 인덱스 시그니처보다 정확한 타입을 사용하고 런타임 때까지 객체의 속성을 알 수 없을 경우에만 인덱스 시그니처를 사용하도록 한다.

예시

CSV 파일처럼 헤더 행에 열 이름이 있고, 데이터 행을 열 이름과 같은 값으로 매핑하는 객체로 나타내고 싶은 경우

function parseCSV(input: string): { [columnName: string]: string }[] {
  const lines = input.split("\n");
  const [header, ...rows] = lines;
  const headerColumns = header.split(",");
  return rows.map((rowStr) => {
    const row: { [columnName: string]: string } = {};
    rowStr.split(",").forEach((cell, i) => {
      row[headerColumns[i]] = cell;
    });
    return row;
  });
}

일반적인 상황에서 열 이름이 무엇인지 미리 알 방법은 없다.
이럴 때는 인덱스 시그니처를 사용한다.

선언해 둔 열들이 런타임에 실제로 일치한다는 보장이 없기에 이를 안전하게 접근하기 위해서는 인덱스 시그니처 값 타입에 undefined를 추가하는 것을 고려해야 한다.

function safeParseCSV(
  input: string
): { [columnName: string]: string | undefined }[] {
  return parseCSV(input);
}

체크를 추가해야 하기에 작업이 조금 번거로울 수 있다. undefined를 타입에 추가할지는 상황에 맞게 판단해야 한다.

타입에 가능한 필드가 제한되어 있는 경우에는 인덱스 시그니처로 모델링하지 말아야 한다.
예를 들어 데이터에 A, B, C, D같은 키가 있지만, 얼마나 많이 있는 지 모른다면 선택적 필드 또는 유니온 타입으로 모델링하면 된다.
유니온 타입은 정확하지만 사용하기 번거롭기에 선택적 필드로 모델링 하는 것이 최선이다.

인덱스 시그니처를 사용하기에 string 타입이 너무 광범위한 경우

Record

키 타입에 유연성을 제공하는 제너릭 타입. string의 부분 집합을 사용할 수 있다.

type Vec3D = Record<"x" | "y" | "z", number>;
// type Vec3D = {
//   x: number;
//   y: number;
//   z: number;
// };

맵드 타입

맵드 타입은 키마다 별도의 타입을 사용하게 해 준다.

type ABC = { [k in "a" | "b" | "c"]: k extends "b" ? string : number };
// type ABC = {
//   a: number;
//   b: string;
//   c: number;
// };

number 인덱스 시그니처보다는 Array, 튜플, ArrayLike를 사용

자바스크립트에서 객체의 키에 숫자는 사용할 수 없다. 숫자를 사용하려고 하면, 자바스크립트 런타임은 문자열로 변환할 것이다.
타입스크립트는 이러한 혼란을 바로잡기 위해 숫자 키를 허용하고, 문자열 키와 다른 것으로 인식한다.

  • 런타임에는 문자열 키로 인식하므로 이는 가상이라고 할 수 있지만, 타입 체크 시점에 오류를 잡을 수 있어서 유용하다.
  • 배열을 순회할 때 인덱스에 신경 쓰지 않는다면 for-of를, 인덱스 타입이 중요하다면 Array.prototype.forEach를 사용하면 된다.
  • 루프 중간에 멈춰야 한다면 for 루프를 사용하는 것이 좋다.
  • 인덱스 시그니처에 number를 사용하기보다 Array나 튜플, ArrayLike를 사용하는 것이 좋다.




0302

변경 관련된 오류 방지를 위해 readonly 사용하기

readonly 접근 제어자를 사용하면 변경 불가한 타입을 선언할 수 있다.
readonly로 배열을 선언하면 일반 배열과 구분되는 몇 가지 특징이 있다.

  • 배열의 요소를 읽을 수 있지만, 쓸 수는 없다.
  • length를 읽을 수 있지만, 바꿀 수는 없다.
  • 배열을 변경하는 pop을 비롯한 다른 메서드를 호출할 수 없다.

number[]readonly number[]보다 기능이 많기 때문에, readonly number[]의 서브타입이 된다. 따라서 변경 가능한 배열을 readonly 배열에 할당할 수 있다. 하지만 그 반대는 불가능하다.

매개변수에서의 readonly

매개변수를 readonly로 선언하면 다음과 같은 일이 생긴다.

  • 타입스크립트는 매개변수가 함수 내에서 변경이 일어나는지 체크한다.
  • 호출하는 쪽에서는 함수가 매개변수를 변경하지 않는다는 보장을 받게 된다.
  • 호출하는 쪽에서 함수에 readonly 배열을 매개변수로 넣을 수도 있다.

만약 함수가 매개변수를 변경하지 않는다면, readonly로 선언해야 한다.
더 넓은 타입으로 호출할 수 있고, 의도치 않은 변경은 방지될 것이다.
다른 라이브러리에 있는 readonly 함수를 호출하는 경우라면, 타입 선언을 바꿀 수 없으므로 타입 단언문을 사용해야 한다.

readonly를 사용하여 지역 변수 변경 오류 방지

소설의 연속된 행을 가져와서 빈 줄을 기준으로 구분되는 단락으로 나누는 기능을 하는 프로그램을 작성했다고 가정해보자

function parseTaggedText(lines: string[]): string[][] {
  const paragraphs: string[][] = [];
  const currPara: string[] = [];

  const addParagraph = () => {
    if (currPara.length) {
      paragraphs.push(currPara);
      currPara.length = 0;
    }
  };

  for (const line of lines) {
    if (!line) {
      addParagraph();
    } else {
      currPara.push(line);
    }
  }
  addParagraph();
  return paragraphs;
}
// [ [], [], [] ] - 의도와 다른 출력

위와 같이 출력되는 이유는 currPara에 새 값을 채우거나 지운다면 동일한 객체를 참조하고 있는 paragraphs 요소에도 변경이 반영되기 때문이다.
currPara를 readonly로 선언하면 이런 동작을 방지할 수 있다.
선언을 바꾸는 즉시 코드 내에서 몇 가지 오류가 발생한다.

function parseTaggedText(lines: string[]): string[][] {
  const paragraphs: string[][] = [];
  const currPara: string[] = [];

  const addParagraph = () => {
    if (currPara.length) {
      paragraphs.push(currPara);
      // 'readonly string[]' 형식의 인수는 'string[]' 형식의 매개 변수에 할당될 수 없습니다.
      currPara.length = 0;
      // 읽기 전용 속성이므로 'length'에 할당할 수 없습니다.
    }
  };

  for (const line of lines) {
    if (!line) {
      addParagraph();
    } else {
      currPara.push(line);
      // 'readonly string[]' 형식에 'push' 속성이 없습니다.
    }
  }
  addParagraph();
  return paragraphs;
}

currPara를 let으로 선언하고 변환이 없는 메서드를 사용함으로써 두 개의 오류를 고칠 수 있다.

let currPara: readonly string[] = [];

currPara = [];

currPara = currPara.concat([line]);

concat은 원본을 수정하지 않고 새 배열을 반환한다.

paragraphs 오류를 바로잡는 방법은 세 가지이다.

  1. currPara의 복사본을 만든다. paragraphs.push([...curPara]); * currPara는 readonly로 유지되지만, 복사본은 원하는 대로 변경이 가능하기 때문에 오류가 사라진다.
  2. paragraphs(그리고 함수의 반환 타입)를 readonly string[]의 배열로 변경한다. const paragraphs: (readonly string[])[] = []; _ 여기서 괄호가 중요한데, readonly string[][]은 readonly 배열의 변경 가능한 배열이 아니라 변경 가능한 배열의 readonly 배열이기 때문이다. _ 이 코드는 동작하지만, readonly를 반환하기 때문에 함수가 반환한 값에 대해 영향을 끼치는 것이 맞는 방법인지 고민해 봐야 한다.
  3. 배열의 readonly 속성을 제거하기 위해 단언문을 사용한다. paragraphs.push(currPara as string[]); * 바로 다음 문장에서 currPara를 새 배열에 할당하므로, 매우 공격적인 단언문은 아닌 것으로 보인다.

얕게 동작하는 readonly

readonly는 얕게 동작한다는 것에 유의하며 사용해야 한다.
객체에 사용되는 Readonly 제너릭도 마찬가지로 얕게 동작한다.
현재 시점에는 deep readonly 타입이 기본적으로 지원되지 않지만, 제너릭을 만들면 사용할 수 있다.
그러나 제너릭은 만들기 까다롭기 때문에 ts-essentials에 있는 DeepReadonly 제너릭과 같은 라이브러리를 사용하는 것이 낫다.

매핑된 타입을 사용하여 값을 동기화하기

산점도(scatter plot)를 그리기 위한 UI 컴포넌트를 작성한다고 가정해보자.
여기에는 디스플레이와 동작을 제어하기 위한 몇 가지 다른 타입의 속성이 포함된다.

interface ScatterProps {
  xs: number[];
  ys: number[];

  xRange: [number, number];
  yRange: [number, number];
  color: string;

  onClick: (x: number, y: number, index: number) => void;
}
  • 불필요한 작업을 피하기 위해, 필요한 때에만 차트를 다시 그릴 수 있다.
  • 다른 속성이 변경되면 다시 그려야 하지만, 이벤트 핸들러가 변경되면 다시 그릴 필요가 없다.
function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps) {
  let k: keyof ScatterProps;

  for (k in oldProps) {
    if (oldProps[k] !== newProps[k]) {
      if (k !== "onClick") return true;
    }
  }
  return false;
}
  • 이와 같이 코드를 작성하면 interface에 다른 이벤트 핸들러가 추가된 경우 값이 변경될 때마다 차트를 다시 그린다.
  • 이렇게 처리하는 것을 보수접 접근법이라고 한다. 또는 실패에 닫힌 접근법이라고 한다.
  • 이 접근법을 이용하면 차트가 정확하지만 너무 자주 그려질 가능성이 있다.
function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps) {
  return (
    oldProps.xs !== newProps.xs ||
    oldProps.ys !== newProps.ys ||
    oldProps.xRange !== newProps.xRange ||
    oldProps.yRange !== newProps.yRange ||
    oldProps.color !== newProps.color
  );
}
  • 속성이 추가되었을 때 차트를 불필요하게 다시 그리는 단점을 해결했다.
  • 이렇게 처리하는 방법을 실패에 열린 접근법이라고 한다.
  • 다만 속성이 추가되었을 때 값이 변경되면 실제로 차트를 다시 그려야 할 경우 누락되는 일이 생길 수 있다.
  • 이러면 속성이 추가될 때마다 ScatterProps 인터페이스를 수정해야 하는데 이는 번거로운 일이다.
const REQUIRES_UPDATE: { [k in keyof ScatterProps]: boolean } = {
  xs: true,
  ys: true,
  xRange: true,
  yRange: true,
  color: true,
  onClick: false,
};

function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps) {
  let k: keyof ScatterProps;

  for (k in oldProps) {
    if (oldProps[k] !== newProps[k] && REQUIRES_UPDATE[k]) {
      return true;
    }
  }
  return false;
}
  • 타입 체커가 동작하도록 개선한 코드이다.
  • [k in keyof ScatterProps]: boolean는 타입 체커에게 ScatterProps와 동일한 속성을 가져야 한다는 정보를 제공한다.
  • 이와 같이 코드를 작성하면 나중에 ScatterProps에 새로운 속성을 추가하는 경우 REQUIRES_UPDATE에 해당 속성이 없는 것을 개발자에게 알려주어 예상치 못한 오류를 발생시킬 확률을 감소시킬 것이다.
  • 이와 같이 매핑된 타입은 한 객체가 다른 객체와 정확히 같은 속성을 가지게 할 때 이상적이다.

추론 가능한 타입을 사용해 장황한 코드 방지

코드의 모든 변수에 타입을 선언하는 것은 비생산적이며 좋지 않다.
타입 추론이 된다면 명시적 타입 구문은 필요하지 않다. 오히려 방해가 되고, 이를 지양하는 것이 좋다.

  • 비구조화 할당문은 모든 지역 변수의 타입이 추론되도록 한다.
  • 함수 내에서 생성된 지역 변수에는 타입 구문을 넣지 않도록 한다. 타입 구문을 생략하여 코드를 읽는 사람이 구현 로직에 집중할 수 있게 하는 것이 좋다.
  • 보통 타입 정보가 있는 라이브러리에서, 콜백 함수의 매개변수 타입은 자동으로 추론된다.

타입을 명시해야 하는 상황

  1. 객체 리터럴을 정의할 때
    • 객체 리터럴 정의에 타입을 명시하면, 잉여 속성 체크가 동작한다.
    • 잉여 속성 체크는 변수가 사용되는 순간이 아닌 할당되는 시점에 오류를 표시해주고, 오류를 잡는 데 효과적이다.
  2. 함수의 반환
    • 타입 추론이 가능할지라도 구현상의 오류가 함수를 호출한 곳까지 영향을 미치지 않도록 하기 위해 타입 구문을 명시하는 게 좋다.
    • 반환 타입을 명시하면, 구현상의 오류가 사용자 코드의 오류로 표시되지 않는다.
    • 또한 함수에 대해 더욱 명확하게 알 수 있고, 명명된 타입으로 더욱 직관적인 표현이 된다.

다른 타입에는 다른 변수 사용하기

let id = "12-34-56";
fetchProduct(id);
id = 123456;
// 'number' 형식은 'string' 형식에 할당할 수 없습니다.

자바스크립트에서는 서로 다른 타입의 값을 하나의 변수에 재할당해도 문제가 없다.
하지만 타입스크립트에서는 처음에 변수의 타입을 string으로 추론하기 때문에 number 값을 재할당하면 문제가 발생한다.

-> 변수의 값은 바귈 수 있지만 그 타입은 보통 바뀌지 않는다.

타입을 바꿀 수 있는 한 가지 방법은 범위를 좁히는 것인데, 타입을 더 작게 제한하는 것이다.
예를 들어, id 변수를 string과 number의 유니온 타입으로 확장하는 것인데, 이보다는 별도의 변수를 도입하는 것이 낫다.
그 이유는,

  • 서로 관련이 없는 두 개의 값을 분리한다.
  • 변수명을 더 구체적으로 지을 수 있다.
  • 타입 추론을 향상시키며, 타입 구문이 불필요해진다.
  • 타입이 유니온보다 더 간결해진다.
  • let 대신 const로 변수를 선언하게 된다.

-> 즉, 타입이 바뀌는 변수는 피해야 하며, 목적이 다른 곳에는 별도의 변수명을 사용해야 한다.




0303

HTTP 메시지

메시지의 흐름

  • HTTP 메시지는 HTTP 애플리케이션 간에 주고받은 데이터의 블록들이다.
  • 이 데이터의 블록들은 메시지의 내용과 의미를 설명하는 텍스트 메타 정보로 시작하고 그 다음에 선택적으로 데이터가 올 수 있다.
  • 이 메시지는 클라이언트, 서버, 프락시 사이를 흐른다.
  • 인바운드, 아웃바운드, 업스트림, 다운스트림은 메시지의 방향을 의미하는 용어다.

메시지는 원 서버 방향을 인바운드로 하여 송신된다

  • HTTP는 인바운드와 아웃바운드라는 용어를 트랜색션 방향을 표현하기 위해 사용한다.
  • 메시지가 원 서버로 향하는 것은 인바운드로 이동하는 것이고, 모든 처리가 끝난 뒤에 메시지가 사용자 에이전트로 돌아오는 것은 아웃바운드로 이동하는 것이다.

다운스트림으로 흐르는 메시지

HTTP 메시지는 강물과 같이 흐른다. 요청 메시지냐 응답 메시지냐에 관계없이 모든 메시지는 다운스트림으로 흐른다.
메시지의 발송자는 수신자의 업스트림이다.

메시지의 각 부분

HTTP 메시지는 단순한, 데이터의 구조화된 블록이다.
메시지는 시작줄, 헤더 블록, 본문 이렇게 세 부분으로 이루어진다.
시작줄은 이것이 어떤 메시지인지 서술하며, 헤더 블록은 속성을, 본문은 데이터를 담고 있다. 이때 본문은 아예 없을 수도 있다.

메시지 문법

모든 HTTP 메시지는 요청 메시지나 응답 메시지로 분류된다.
요청 메시지는 웹 서버에 어떤 동작을 요구하고, 응답 메시지는 요청의 결과를 클라이언트에게 돌려준다.

둘의 형식은 시작줄에서만 문법이 다르다.

요청 메시지

<메서드> <요청 URL> <버전>
<헤더>

<엔터티 본문>

응답 메시지

<버전> <상태 코드> <사유 구절>
<헤더>

<엔터티 본문>
  • 메서드 : 클라이언트 측에서 서버가 리소스에 대해 수행해주길 바라는 동작
  • 요청 URL : 요청 대상이 되는 리소스를 지칭하는 URL
  • 버전 : 메시지에서 사용 중인 HTTP의 버전
  • 상태 코드 : 요청 중에 무엇이 일어났는지 설명하는 세 자리 숫자.
  • 사유 구절 : 숫자로 된 상태코드의 의미를 사람이 이해할 수있게 설명해주는 짧은 문구

시작줄

모든 HTTP 메시지는 시작줄로 시작한다.
요청 메시지의 시작줄은 무엇을 해야 하는지 말해준다.
응답 메시지의 시작줄은 무슨 일이 일어났는지 말해준다.
모든 필드는 공백으로 구분된다.

메서드
요청의 시작줄은 메서드로 시작하며, 서버에게 무엇을 해야 하는지 말해준다.
모든 서버가 모든 종류의 메서드를 구현한 것은 아니다.
그리고 HTTP는 쉽게 확장할 수 있도록 설계되었기 때문에, 다른 서버는 그들만의 메서드를 추가로 구현했을 수도 있다. 이를 확장 메서드라고 한다.

상태 코드
메서드가 서버에게 무엇을 해야 하는지 말해주는 것처럼, 상태 코드는 클라이언트에게 무엇이 일어났는지 말해준다.
상태 코드는 숫자로 된 코드와, 문자열로 되어 있어서 사람이 이해하기 쉬운 메시지 두 형태 모두로 반환된다.
사유 구절이 사람에게 쉽게 읽히는 한편, 숫자로 된 코드는 프로그램이 에러를 처리하기 쉽다.

사유 구절
상태 코드에 대한 글로 된 설명을 제공하며 상태 코드의 사람이 이해하기 쉬운 버전이다.

버전 번호
애플리케이션들이 자신이 따르는 프로토콜의 버전을 상대방에게 말해주기 위한 수단이 된다.
버전 번호는 HTTP로 대화하는 애플리케이션들에게 대화 상대의 능력과 메시지에 대한 단서를 제공해주기 위한 것이다.
그리고 버전 번호는 어떤 애플리케이션이 지원하는 가장 높은 HTTP 버전을 가리킨다.

헤더

HTTP 헤더 필드는 요청과 응답 메시지에 추가 정보를 더한다.
이는 기본적으로 이름/값 쌍의 목록이다.

헤더 분류

  • 일반 헤더: 요청과 응답 양쪽에 모두 나타날 수 있음
  • 요청 헤더: 요청에 대한 부가 정보를 제공
  • 응답 헤더: 응답에 대한 부가 정보를 제공
  • Entity 헤더: 본문 크기와 콘텐츠, 혹은 리소스 그 자체를 서술
  • 확장 헤더: 명세에 정의되지 않는 새로운 헤더

엔터티 본문

엔터티 본문은 HTTP가 수송하도록 설계된 것들이다.
HTTP 메시지는 이미지, 비디오, HTML 문서, 소프트웨어 애플리케이션, 신용카드 트랜잭션, 전자우편 등 여러 종류의 디지털 데이터를 실어 나를 수 있다.

버전 0.9 메시지

HTTP 버전 0.9는 HTTP 프로토콜의 초기 버전이다. HTTP가 갖고 있는 요청과 응답 메시지의 시초이지만, 훨씬 단순한 프로토콜로 되어 있다.
지나칠 정도의 단순함 때문에, 다양한 상황에 대응할 수 없다.

메서드

안전한 메서드

HTTP는 안전한 메서드라 불리는 메서드의 집합을 정의한다.
GET과 HEAD 메서드는 안전하다고 할 수 있는데, 이는 GET과 HEAD 메서드를 사용하는 HTTP 요청의 결과로 서버에 어떤 작용도 없음을 의미한다.
서버에 작용이 없다는 것은, HTTP 요청의 결과로 인해 서버에서 일어나는 일은 아무것도 없다는 의미이다.
안전한 메서드의 목적은, 서버에 어떤 영향을 줄 수 있는 안전하지 않은 메서드가 사용될 때 사용자들에게 그 사실을 알려주는 HTTP 애플리케이션을 만들 수 있도록 하는 것에 있다.

GET

가장 흔히 쓰이는 메서드로 주로 서버에게 리소스를 달라고 요청하기 위해 쓰인다.

GET처럼 행동하지만, 서버는 응답으로 헤더만을 돌려준다. 엔터티 본문은 결코 반환되지 않는다.
이는 클라이언트가 리소스를 실제로 가져올 필요 없이 헤더만을 조사할 수 있도록 해준다.

이를 사용하면

  • 리소스를 가져오지 않고도 그에 대해 무엇인가를 알아낼 수 있다.
  • 응답의 상태 코드를 통해, 개체가 존재하는지 확인할 수 있다.
  • 헤더를 확인하여 리소스가 변경되었는지 검사할 수 있다.

PUT

PUT 메서드는 서버에 문서를 쓴다.
서버가 요청의 본문을 가지고 요청 URL의 이름대로 새 문서를 만들거나, 이미 URL이 존재한다면 본문을 사용해서 교체하는 것이다.

POST

서버에 입력 데이터를 전송하기 위해 설계되었다.

TRACE

클라이언트가 어떤 요청을 할 때, 그 요청은 방화벽, 프락시, 게이트웨이 등의 애플리케이션을 통과할 수 있다.
이들에게 원래의 HTTP 요청을 수정할 수 있는 기회가 있다.
TRACE 메서드는 클라이언트에게 자신의 요청이 서버에 도달했을 때 어떻게 보이게 되는지 알려준다.

  • TRACE 요청은 목적지 서버에서 루프백 진단을 시작한다.
  • 요청 전송의 마지막 단계에 있는 서버는 자신이 받은 요청 메시지를 본문에 넣어 TRACE 응답을 되돌려준다.
  • 클라이언트는 자신과 목적지 서버 사이에 있는 모든 HTTP 애플리케이션의 요청/응답 연쇄를 따라가면서 자신이 보낸 메시지가 망가졌거나 수정되었는지, 만약 그렇다면 어떻게 변경되었는지 확인할 수 있다.
  • 이는 주로 진단을 위해 사용된다. 프락시나 다른 애플리케이션들이 요청에 어떤 영향을 미치는지 확인해보고자 할 때도 좋은 도구다.
  • TRACE는 메서드를 구별하는 메커니즘을 제공하지 않기에 중간 애플리케이션이 여러 다른 종류의 요청들을 일관되게 다룬다고 가정하는 문제가 있다.

OPTIONS

웹 서버에게 여러 가지 종류의 지원 범위에 대해 물어본다.
이 메서드는 여러 리소스에 대해 실제로 접근하지 않고도 그것들을 어떻게 접근하는 것이 최선인지 확인할 수 있는 수단을 클라이언트 애플리케이션에게 제공한다.

DELETE

서버에게 요청한 URL로 지정한 리소스를 삭제할 것을 요청한다.
그러나 클라이언트는 삭제가 수행되는 것을 보장하지 못한다.

확장 메서드

HTTP는 필요에 따라 확장해도 문제가 없도록 설계되어 있으므로, 새로 기능을 추가해도 과거에 구현된 소프트웨어들의 오동작을 유발하지 않는다.
확장 메서드는 명세에 정의되지 않은 메서드로 개발자들에게 그들의 서버가 구현한 HTTP 서비스의 서버가 관리하는 리소스에 대한 능력을 확장하는 수단을 제공한다.

상태 코드

클라이언트에게 그들의 트랜잭션을 이해할 수 있는 쉬운 방법을 제공한다.

100-199: 정보성 상태 코드

100 - Continue
요청의 시작 부분 일부가 받아들여졌으며, 클라이언트는 나머지를 계속 이어서 보내야 함을 의미.
이를 보낸 후, 서버는 반드시 요청을 받아 응답해야 한다.
서버에 엔터티 본문을 전송하기 전에 그 엔터티 본문을 서버가 받아들일 것인지 확인하려고 할 때, 그 확인 작업을 최적화하기 위한 의도로 도입

만약 클라이언트가 엔터티를 서버에게 보내려고 하고, 그 전에 100 Continue 응답을 기다리겠다면, 클라이언트는 값을 100-continue로 하는 Expect 요청 헤더를 보낼 필요가 있다.
이는 여러 측면에서 최적화를 위한 것으로 100-continue를 서버가 다루거나 사용할 수 없는 큰 엔터티를 서버에게 보내지 않으려는 목적으로만 사용해야 한다.
클라이언트 개발자는 예상하지 못한 100 Continue 응답에도 대비해야 한다. 몇몇 잘못 만들어진 HTTP 애플리케이션은 이 코드를 부적절하게 보낸다.

200-299: 성공 상태 코드

서버는 대응하는 성공을 의미하는 상태 코드의 배열을 갖고 있으며, 각각 다른 종류의 요청에 대응한다.

200 - OK
요청은 정상이고, 엔터티 본문은 요청한 리소스를 포함하고 있다.

201 - Created
서버 개체를 생성하라는 요청을 위한 것.
응답은 생성된 리소스에 대한 최대한 구체적인 참조가 담긴 Location 헤더와 함께, 그 리소스를 참조할 수 있는 여러 URL을 엔터티 본문에 포함해야 한다.
서버는 상태 코드를 보내기에 앞서 반드시 객체를 생성해야 한다.

300-399: 리다이렉션 상태 코드

클라이언트가 관심있어 하는 리소스에 대해 다른 위치를 사용하라고 말해주거나 그 리소스의 내용 대신 다른 대안 응답을 제공한다.
만약 리소스가 옮겨졌다면, 클라이언트에게 리소스가 옮겨졌으며 어디서 찾을 수 있는지 알려주기 위해 리다이렉션 상태 코드와 (선택적으로) Location 헤더를 보낼 수 있다.
웹 브라우저는 3xx 응답 결과에 Location 헤더가 있으면, 이 위치로 자동 이동한다.

리다이렉션 상태 코드 중 몇몇은 리소스에 대한 애플리케이션의 로컬 복사본이 원래 서버와 비교했을 때 유효한지 확인하기 위해 사용된다.
ex) 특정 시점 이후 수정된 경우에만 문서를 가져오라고 말하기 위해 If-Modified-Since 헤더를 전송하면 문서가 그 날짜 이후 변한 것이 없다면, 서버는 콘텐츠 대신 304 상태 코드로 답한다.

일반적으로, HEAD가 아닌 요청에 대해 리다이렉션 상태 코드를 포함한 응답을 할 때, 리다이렉트될 URL에 대한 링크와 설명을 포함시키는 것은 좋은 습관이다.

301 - Moved Permanently
요청된 URL이 옮겨졌을 때 사용한다.
리다이렉트시 요청 메서드가 GET으로 변한다.

302 - Found
클라이언트는 Location 헤더로 주어진 URL을 리소스를 임시로 가리키기 위한 목적으로 사용해야 한다.
처음 302 스펙의 의도는 HTTP 메서드를 유지하는 것이었지만 웹 브라우저들이 대부분 GET으로 바꾸어버렸다.
그래서 모호한 302를 대신하는 명확한 307(메서드가 변하면 안됨), 303(메서드가 GET으로 변경)이 등장했고 권장하지만 현실적으로 이미 많은 애플리케이션들이 302를 기본값으로 사용한다.

PRG: Post/Redirect/GET - 일시적인 리다이렉션 예시

  • POST로 주문후에 새로 고침으로 인한 중복 주문 방지
  • POST로 주문후에 주문 결과 화면을 GET 메서드로 리다이렉트
  • 새로고침해도 결과 화면을 GET으로 조회
  • 중복 주문 대신에 결과 화면만 GET으로 다시 요청

304 - Not Modified
클라이언트는 헤더를 이용해 조건부 요청을 만들 수 있다.
만약 클라이언트가 GET과 같은 조건부 요청을 보냈고 그 요청한 리소스가 최근에 수정된 일이 없다면, 이 코드는 리소스가 수정되지 않았음을 의미하게 된다.
이 상태 코드를 동반한 응답은 엔터티 본문을 가져서는 안 된다. (로컬 캐시를 사용해야 하므로 캐시로 리다이렉트 한다.)
조건부 GET, HEAD 요청시 사용하며 주로 캐시를 목적으로 사용한다.

400-499: 클라이언트 에러 상태 코드

클라이언트의 요청에 잘못된 문법등으로 서버가 요청을 수행할 수 없는 경우 사용
클라이언트가 이미 잘못된 요청, 데이터를 보내고 있기 때문에, 똑같은 재시도가 실패함. 즉, 요청을 수정해서 보내야 한다.

400 - Bad Request
클라이언트가 잘못된 요청을 해서 서버가 처리할 수 없음
클라이언트는 요청 내용을 다시 검토하고 보내야 함

401 - Unauthorized
클라이언트가 해당 리소스에 대한 인증이 필요하다.
응답에 WWW-Authenticate 헤더와 함께 인증 방법을 설명

403 - Forbiddden 요청이 서버에 의해 거부되었음을 알려주기 위해 사용한다.
주로 인증 자격 증명(로그인)은 있지만, 접근 권한이 불충분한 경우 사용한다.

404 - Not Found
서버가 요청한 URL을 찾을 수 없음을 알려주기 위해 사용한다.

500-599: 서버 에러 상태 코드

서버 문제로 발생하는 오류에 대한 상태 코드로 서버의 제한에 걸린 것일 수도 있고, 혹은 게이트웨이 리소스와 같은 서버의 보조 구성요소에서 발생한 에러일 수도 있다.
서버에 문제가 있기 때문에 재시도하면 성공할 수도 있음

500 - Internal Server Error
서버가 요청을 처리할 수 없게 만드는 에러를 만났을 때 사용한다.

503 - Service Unavailable
서버가 일시적인 과부하 또는 예정된 작업으로 잠시 요청을 처리할 수 없음
Retry-After 헤더 필드로 얼마 뒤에 복구되는지 보낼 수도 있음

헤더

헤더에는 특정 종류의 메시지에만 사용할 수 있는 헤더와, 더 일반 목적으로 사용할 수 있는 헤더, 그리고 응답과 요청 메시지 양쪽 모드에서 정보를 제공하는 헤더가 있다.

일반 헤더

클라이언트와 서버 양쪽 모두가 사용하는 헤더이다.
메시지에 대한 아주 기본적인 정보를 제공하며 유용한 정보를 제공한다.

Date
메시지가 언제 만들어졌는지에 대한 날짜와 시간 제공

Cache-Control
메시지와 함께 캐시 지시자를 전달하기 위해 사용한다.

요청 헤더

요청 메시지를 위한 헤더로 서버에게 클라이언트가 받고자 하는 데이터의 타입이 무엇인지와 같은 부가 정보를 제공한다.

Host
요청의 대상이 되는 서버의 호스트 명과 포트를 준다.
하나의 IP 주소에 여러 도메인이 적용되어 있을 때 사용된다.

Referer
현재 요청된 페이지의 이전 웹 페이지 주소
Referer를 사용해서 유입 경로 분석이 가능하다.

Accept 관련 헤더
클라이언트는 Accept 관련 헤더들을 이용해 서버에게 자신의 선호와 능력을 알려줄 수 있다.
서버는 이 추가 정보를 활용해서 무엇을 보낼 것인가에 대해 더 똑똑한 결정을 내릴 수 있다.
이를 통해 클라이언트는 원하는 것을 얻을 수 있으며, 서버는 클라이언트가 사용할 수 없는 것을 전송하는 데 시간과 대역폭을 낭비하지 않을 수 있다.

  • Accept: 클라이언트가 선호하는 미디어 타입 전달
  • Accept-Charset: 클라이언트가 선호하는 문자 인코딩
  • Accept-Encoding: 클라이언트가 선호하는 압축 인코딩
  • Accept-Language: 클라이언트가 선호하는 자연 언어

조건부 요청 헤더
클라이언트가 어떤 문서의 사본을 가지고 있는 상태라면, 클라이언트는 서버에게 그 문서를 요청할 때 자신이 갖고 있는 사본과 다를 때만 전송해 달라고 요청하고 싶을 수 있다.
조건부 요청 헤더를 사용하면, 클라이언트는 서버에게 요청에 응답하기 전에 먼저 조건이 참인지 확인하게 하는 제약을 포함시킬 수 있다.

  • Expect: 클라이언트가 요청에 필요한 서버의 행동을 열거할 수 있게 해준다.
  • If-None-Match: 문서의 엔터티 태그가 주어진 엔터티 태그와 일치하지 않는 경우에만 문서를 가져온다.
  • If-Modified-Since: 주어진 날짜 이후에 리소스가 변경된 경우에만 문서를 가져온다

요청 보안 헤더
HTTP는 자체적으로 요청을 위한 간단한 인증요구/응답 체계를 갖고 있다.
요청하는 클라이언트가 어느 정도의 리로스에 접근하기 전에 자신을 인증하게 함으로써 트랜잭션을 더 안전하게 만ㄷ르고자 한다.

  • Authorization: 클라이언트가 서버에게 제공하는 인증 그 자체에 대한 정보를 담고 있다.
  • Cookie: 클라이언트가 서버에게 토큰을 전달할 때 사용한다.

프락시 요청 헤더
프락시의 기능을 돕기 위한 헤더

응답 헤더

응답 메시지는 클라이언트에게 정보를 제공하기 위한 자신만의 헤더를 갖고 있다.
이는 클라이언트에게 부가 정보를 제공한다.

Server
요청을 처리하는 서버 애플리케이션의 이름과 버전

Retry-After
현재 리소스가 사용 불가능한 상태일 때, 언제 가능해지는지 날짜 또는 시각

협상 헤더
서버에 여러 언어로 번역된 문서가 있는 경우 여러 가지 표현이 가능한 상황이라면, 어떤 표현을 택할 것인가에 대해 협상을 할 수 있도록 지원한다.

응답 보안 헤더

  • set-Cookie: 서버가 클라이언트를 인증할 수 있도록 클라이언트 측에 토큰을 설정하기 위해 사용
  • WWW-Authenticate: 서버에서 클라이언트로 보낸 인증 요구의 목록

엔터티 헤더

엔터티 본문에 대한 헤더로 엔터티 본문에 들어있는 데이터의 타입이 무엇인지 말해줄 수 있다.
요청과 응답 양타입의 메시지에 모두 나타날 수 있다.
일반적으로 엔터티 헤더는 수신자에게 자신이 다루고 있는 것이 무엇인지 말해준다.

Location
클라이언트에게 엔터티가 실제로 어디에 위치하고 있는지 말해준다.
웹 브라우저는 3xx 응답 결과에 Location 헤더가 있으면, 해당 위치로 자동 이동시킨다.

콘텐츠 헤더
엔터티의 콘텐츠에 대한 구체적인 정보를 제공한다.

  • Content-Type: 표현 데이터의 형식
  • Content-Encoding: 표현 데이터에 적용된 인코딩
  • Content-Language: 표현 데이터를 이해하는데 가장 적절한 자연어
  • Content-Length: 표현 데이터의 길이

엔터티 캐싱 헤더
엔터티 캐싱에 대한 정보를 제공

  • ETag: 엔터티에 대한 엔터티 태그
  • Last-Modified: 가장 최근 이 엔터티가 변경된 일시




0304

컴포넌트 & props

컴포넌트는 UI를 구성하는 조각에 해당하며, 독립적으로 분리되어 재사용을 목적으로 사용한다.
React 앱에서 컴포넌트는 개별적인 파일로 분리되어 관리한다.
버튼, 검색 폼, 캘린더, 모달 다이얼로그 등은 대표적인 컴포넌트의 예로 앱의 여러 부분에서 재사용된다.

컴포넌트 트리
컴포넌트는 다른 컴포넌트를 내부에 포함할 수 있다. 포함된 컴포넌트는 하위 컴포넌트, 포함하는 컴포넌트는 상위 컴포넌트가 된다.

컴포넌트 타입

컴포넌트는 재사용을 목적으로 설계한다.
ECMAScript에서는 재사용 가능한 설계 구현을 위해 함수 또는 클래스를 사용한다.
함수, 클래스 타입이 재사용을 위한 React 컴포넌트의 타입이다.

함수 컴포넌트

React 컴포넌트는 개념상 JS 함수와 유사하다.
컴포넌트 외부로부터 속성(props)을 전달 받아 어떻게 UI를 구성해야 할지 설정하여 React 요소(JSX를 바벨이 변환 처리)로 반환한다.

import React from "react";

export default function BaseButton(props) {
  return (
    <button type={props.type} className="base-button">
      {props.children}
    </button>
  );
}

클래스 컴포넌트

클래스 문법을 사용해 컴포넌트를 정의할 수도 있다.
함수 컴포넌트와 달리, React의 Component를 확장해 사용해야 한다.

import React from "react";

class BaseButton extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    <button type={this.props.type} className="base-button">
      {this.props.children}
    </button>;
  }
}

컴포넌트 렌더링

컴포넌트는 React 요소로서 렌더링 된다.
클래스의 인스턴스를 만드는 것과 같다.
React API를 사용할 경우 컴포넌트 참조를 타입으로 설정하고 props 객체를 전달한다.
JSX를 사용하면 코드 작성을 보다 쉽게 할 수 있다. 컴포넌트 이름은 파스칼 문법을 사용해야 한다.

props

JSX 속성은 리액트 컴포넌트에 props 객체로 전달된다.

React.createElement(
  BaseButton,
  {
    type: 'reset'
    children: [
      React.createElement(
        'span',
        { className: 'icon icon--reset', 'aria-hidden': 'true' }
      ),
      React.createElement(
        'span',
        { className: 'a11yHidden', children: '초기화' }
      )
    ]
  }
)




0305

타입 넓히기

상수를 사용해서 변수를 초기화할 때 타입을 명시하지 않으면 타입 체커는 타입을 결정해야 한다.
지정된 단일 값을 가지고 할당 가능한 값들의 집합을 유추해야 하는데 이러한 과정을 ‘넓히기’라고 부른다.
넓히기의 과정을 이해한다면 오류의 원인을 파악하고 타입 구문을 더 효과적으로 사용할 수 있다.

function getComponent(vector: Vector3, axis: "x" | "y" | "z") {
  return vector[axis];
}

let x = "x";
let vec = { x: 10, y: 10, z: 10 };
getComponent(vec, x);
// 'string' 형식의 인수는 '"x" | "y" | "z"' 형식의 매개 변수에 할당될 수 없습니다.
  • x의 타입은 할당 시점에 넓히기가 동작해서 string으로 추론되었다.
  • string 타입은 ‘x’ ‘y’ ‘z’ 타입에 할당 불가능하므로 오류가 된 것이다.
  • 이와 같이 타입 넓히기가 진행될 때, 주어진 값으로 추론 가능한 타입이 여러 개이기 때문에 과정이 상당히 모호하다.
const mixed = ["x", 1];
  • 위의 코드 역시 상당히 많은 타입으로 추론될 수 있지만 이 경우에는 (string | number)[]으로 추측한다.
  • 타입스크립트는 타입을 추론할 때 명확성과 유연성 사이의 균형을 유지하려 한다.

넓히기 과정 제어 방법

const

  • let 대신 const로 변수를 선언하면 더 좁은 타입이 된다.
  • 변수는 재할당될 수 없으므로 더 좁은 타입으로 추론 가능하다.
  • 다만 객체와 배열의 경우 여전히 문제가 존재한다.
  • 객체의 경우 타입스크립트의 넓히기 알고리즘은 각 요소를 let으로 할당된 것처럼 다룬다.
    • 덕분에 속성을 재할당할 수 있지만 다른 타입으로 변경은 불가능하다.
    • 그리고 다른 속성을 추가하지도 못해 객체를 한번에 만들어야 한다.

명시적 타입 구문 제공

const v: { x: 1 | 3 | 5 } = {
  x: 1,
};
// 타입이 { x: 1 | 3 | 5; }

타입 체커에 추가적인 문맥 제공

예를 들어, 함수의 매개변수로 값을 전달

const 단언문

  • const 단언문은 온전히 타입 공간의 기법으로 변수 선언에 쓰이는 const와는 다르다.
  • 값 뒤에 as const를 작성하면, 타입스크립트는 최대한 좁은 타입으로 추론한다.
  • 배열을 튜플 타입으로 추론할 때에도 as const를 사용할 수 있다.
  • 넓히기로 인해 오류가 발생한다고 생각되면, 명시적 타입 구문 또는 const 단언문을 추가하는 것을 고려해야 한다.
const v1 = {
  x: 1 as const,
  y: 2,
};
// 타입은 { x: 1; y: number; }

const v2 = {
  x: 1,
  y: 2,
} as const;
// 타입은 { readonly x: 1; readonly y: 2; }

타입 좁히기

타입 넓히기의 반대는 타입 좁히기로 타입 스크립트가 넓은 타입으로부터 좁은 타입으로 진행하는 과정을 말한다.

타입 좁히기의 여러 방법

null 체크

  • 분기문으로 null을 제외하면, 더 좁은 타입이 되어 작업이 훨씬 쉬워진다.
  • 분기문에서 예외를 던지거나 함수를 반환하여 블록의 나머지 부분에서 변수의 타입을 좁힐 수도 있다.

instanceof

function contains(text: string, search: string | RegExp) {
  if (search instanceof RegExp) {
    // 타입이 RegExp
    return !!search.exec(text);
  }
  // 타입이 string
  return text.includes(search);
}

속성 체크

function pickAB(ab: A | B) {
  if ("a" in ab) {
    // 타입이 A
  } else {
    // 타입이 B
  }
  // 타입이 A | B
}
  • Array.isArray 같은 일부 내장 함수로도 타입을 좁힐 수 있다.

명시적인 태그

interface UploadEvent {
  type: "upload";
  filename: string;
  contents: string;
}

interface DownloadEvent {
  type: "download";
  filename: string;
}

type AppEvent = UploadEvent | DownloadEvent;

function handleEvent(e: AppEvent) {
  switch (e.type) {
    case "download":
      // 타입이 DownloadEvent
      break;
    case "upload":
      // 타입이 UploadEvent
      break;
  }
}
  • 이 패턴은 태그된 유니온 또는 구별된 유니온이라고 불리며, 타입스크립트에서 자주 찾아볼 수 있다.

사용자 정의 타입 가드

function isInputElement(el: HTMLElement): el is HTMLInputElement {
  return "value" in el;
}
  • 반환 타입의 el is HTMLInputElement 는 함수의 반환이 true인 경우, 타입 체커에게 매개변수의 타입을 좁힐 수 있다고 알려 준다.
  • 타입 가드를 사용해서 배열과 객체의 타입 좁히기를 할 수도 있다.
    • 배열에서 어떤 탐색을 수행할 때 undefined가 될 수 있는 타입을 사용할 수 있다.
    • 이 때, filter 함수를 사용해 undefined를 걸러 내려고 해도 잘 동작하지 않는다.
    • 이럴 때 타입 가드를 사용하면 타입을 좁힐 수 있다.
function isDefined<T>(x: T | undefined): x is T {
  return x !== undefined;
}

저지르기 쉬운 실수

const el = document.getElementById("foo");
if (typeof el === "object") {
  // 타입이 HTMLElement | null
}
  • 자바스크립트에서 typeof null이 object이기 때문에, if 구문에서 null이 제외되지 않았다.
function foo(x?: number | string | null) {
  if (!x) {
    // 타입이 stirng | number| null | undefined
  }
}
  • 빈 문자열과 0 모두 false가 되기 때문에, 타입은 전혀 좁혀지지 않았고 x는 여전히 블록 내에서 string 또는 number가 된다.

태그:

카테고리:

업데이트: