0404 - 0410
0404
타입스크립트 기능보다는 ECMAScript 기능 사용하기
기존 자바스크립트는 결함이 많고 개선해야 할 부분이 많아서, 타입스크립트는 초기 버전에 독립적으로 개발한 클래스, 열거형, 모듈 시스템을 포함시킬 수밖에 없었다.
시간이 흐르며 대부분 JS 내장 기능으로 추가했으나, 새로 추가된 기능들은 기존 TS 기능과 호환성 문제를 발생시켰다.
결국 TC39는 런타임 기능을 발전시키고, TS 팀은 타입 기능만 발전시킨다는 명확한 원칙을 세우고 현재까지 지켜오고 있다.
이 원칙이 세워지기 전에, 이미 사용되고 있던 몇 가지 기능이 있는데, 이는 타입 공간과 값 공간의 경계를 혼란스럽게 만들기 때문에 사용하지 않는 것이 좋다.
열거형(enum)
많은 언어에서 몇몇 값의 모음을 나타내기 위해 열거형을 사용한다.
enum Flavor {
VANILLA = 0,
CHOCOLATE = 1,
STRAWBERRY = 2,
}
let flavor = Flavor.CHOCOLATE; // 타입이 Flavor
- 단순히 값을 나열하는 것보다 실수가 적고 명확하기 때문에 일반적으로 열거형을 사용하는 것이 좋다.
- 그러나 TS의 열거형은 몇 가지 문제가 있다.
- 숫자 열거형에 0, 1, 2 외의 다른 숫자가 할당되면 매우 위험하다.
- 상수 열거형은 보통의 열거형과 달리 런타임에 완전히 제거된다. 위의 예제를 const enum Flavor로 바꾸면, 컴파일러는 Flavor.CHOCOLATE을 1으로 바꿔 버린다.
- 문자열 열거형은 런타임의 타입 안전성과 투명성을 제공한다. 그러나 타입스크립트의 다른 타입과 달리 구조적 타이핑이 아닌 명목적 타이핑을 사용한다.
- 명목적 타이핑은 타입의 이름이 같아야 할당이 허용된다.
이처럼 JS와 TS에서 동작이 다르기 때문에 문자열 열거형은 사용하지 않는 것이 좋다. 열거형 대신 리터럴 타입의 유니온을 사용하면 된다.
type Flavor = "vanilla" | "chocolate" | "strawberry";
const flavor: Flavor = "chocolate";
- 리터럴 타입의 유니온은 열거형만큼 안전하며 JS와 호환되는 장점이 있다.
매개변수 속성
일반적으로 클래스를 초기화할 때 속성을 할당하기 위해 생성자의 매개변수를 사용한다.
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
타입스크립트는 더 간결한 문법을 제공한다.
class Person {
constructor(public name: string) {}
}
- public name은 ‘매개변수 속성’이라고 불리며, 간단하지만 몇 가지 문제점이 존재한다.
- 일반적으로 타입스크립트 컴파일은 타입 제거가 이루어지므로 코드가 줄어들지만, 매개변수 속성은 코드가 늘어나는 문법이다.
- 매개변수 속성이 런타임에는 실제로 사용되지만, TS 관점에서는 사용되지 않는 것처럼 보인다.
- 매개변수 속성과 일반 속성을 섞어서 사용하면 클래스 설계가 혼란스러워진다.
- 클래스에 매개변수 속성만 존재한다면 클래스 대신 인터페이스로 만들고 객체 리터럴을 사용하는 것이 좋다.
네임스페이스와 트리플 슬래시 임포트
ES6 이전에는 JS에 공식적인 모듈 시스템이 없었다. 그래서 각 환경마다 모듈 시스템을 마련했다.
TS 역시 자체적인 모듈 시스템을 구축했고, module 키워드와 ‘트리플 슬래시’임포트를 사용했다.
이후 ES6가 모듈 시스템을 도입한 이후, TS는 충돌을 피하기 위해 module과 같은 기능을 하는 namespace 키워드를 추가했다.
namespace foo {
function bar() {}
}
/// <reference path="other.ts"/>
foo.bar();
- 트리플 슬래시 임포트와 module 키워드는 호환성을 위해 남아 있을 뿐이며, 이제는 ES6 스타일의 모듈을 사용해야 한다.
데코레이터
데코레이터는 클래스, 메서드, 속성에 애너테이션을 붙이거나 기능을 추가하는 데 사용할 수 있다.
예를 들어, 클래스의 메서드가 호출될 때마다 로그를 남기려면 logged 애너테이션을 정의할 수 있다.
데코레이터는 처음에 앵귤러 프레임워크를 지원하기 위해 추가되었으며, 앵귤러를 사용하거나 애너테이션이 필요한 프레임워크를 사용하고 있는 게 아니라면 데코레이터가 표준이 되기 전에는 TS에서 데코레이터를 사용하지 않는 게 좋다.
객체를 순회하는 노하우
const obj = {
one: "uno",
two: "dos",
three: "tres",
};
for (const k in obj) {
const v = obj[k];
}
- 이 예시는 정상적으로 실행되지만, 편집기에서는 오류가 발생한다.
- k의 타입은 string인 반면, obj 객체에는 ‘one’, ‘two’, ‘three’ 세 개의 키만 존재한다. k와 obj 객체의 키 타입이 서로 다르게 추론되어 오류가 발생한 것이다.
- k의 타입을 구체적으로 명시해 주면 오류는 사라진다.
const obj = {
one: "uno",
two: "dos",
three: "tres",
};
let k: keyof typeof obj;
for (k in obj) {
const v = obj[k];
}
- obj에는 ‘one’, ‘two’, ‘three’ 외의 추가키를 가진 객체가 할당될 수도 있으므로 이와 같은 오류가 발생한다.
- keyof 키워드를 사용한 방법은 v가
string | number
타입으로 한정되어 범위가 너무 좁아 문제가 될 수 있다. - 추가적인 키를 가진 객체가 할당될 수 있기에 v의 타입을 이러게 추론한 것은 잘못이며 런타임 동작을 예상하기 어렵다.
단지 객체의 키와 값을 순회하고 싶다면 Object.entries
를 사용하면 된다.
interface ABC {
a: string;
b: string;
c: number;
}
function foo(abc: ABC) {
for (const [k, v] of Object.entries(abc)) {
k; // string 타입
v; // any 타입
}
}
- 객체를 다룰 때는 항상 프로토타입 오염의 가능성을 염두에 두어야 한다.
- for-in 문을 사용하면, 객체의 정의에 없는 속성이 갑자기 등장할 수 있다.
- 실제 작업에서는 Object.prototype에 순회 가능한 속성을 절대로 추가하면 안 된다.
- kyeof 선언은 상수이거나 추가적인 키 없이 정확한 타입을 원하는 경우 적절하고, Object.entries는 더욱 일반적으로 쓰이지만 키와 값의 타입을 다루기 까다롭다.
0405
useRef
const refContainer = useRef(initialValue);
useRef는 current 프로퍼티가 전달된 인자(initialValue)로 초기화된 변경 가능한 ref 객체를 반환한다.
반환된 객체는 컴포넌트의 전 생애주기를 통해 유지된다.
React에서 ref는 주로 DOM 노드 참조 목적으로 사용되지만, 컴포넌트 렌더링에 영향을 주지 않는 값 참조 목적으로 사용되기도 한다.
React.useRef()
훅은 함수 컴포넌트 내부에서 가변 값을 유지하는 데에 편리하다.
개념적으로, class의 인스턴스 변수와 ref를 비슷하게 생각할 수 있다.
useRef는 순수 자바스크립트 객체를 생성하는데, 매번 렌더링을 할 때 동일한 ref 객체를 제공한다.
React.useState()
훅과 달리, useRef()
훅은 현재 값이 변경되어도 컴포넌트가 다시 렌더링되지 않아서 불필요한 렌더링을 방지하고, 애플리케이션 성능을 최적화 할 수 있다.
function Timer() {
const intervalRef = useRef();
useEffect(() => {
const id = setInterval(() => {
// ...
});
intervalRef.current = id;
return () => {
clearInterval(intervalRef.current);
};
});
// ...
}
인터벌을 설정하고 싶다면 ref가 필요하지 않지만, 이벤트 처리에서 인터벌을 지우고 싶을 때 유용하다.
function handleCancelClick() {
clearInterval(intervalRef.current);
}
지연 초기화를 수행하지 않는 한, 렌더링 중에 ref 설정을 피해야 한다.
일반적으로 이벤트 처리와 effect에서 ref를 수정하는 것이 좋다.
useLayoutEffect
useEffect 훅과 사용법이 동일하다. 거의 비슷하지만 실행되는 시기가 다르다.
페이지 로드 차단을 방지하기 위해 DOM이 렌더링 된 이후 useEffect 훅에 설정된 콜백함수가 실행되면 위치 및 스타일 적용에 문제가 생길 수 있다.
이러한 문제를 해결해야 할 경우 useLayoutEffect 훅을 사용한다.
useLayoutEffect 훅은 DOM이 렌더링 되고 페인팅 되기 직전에 동기적으로 실행된다. 반면, useEffect는 DOM 페인팅 이후 비동기적으로 실행된다.
그렇기에 useEffect는 DOM에 영향을 미치는 코드가 있다면 다시 그리므로 화면 깜빡임이 발생한다.
반면, useLayoutEffect는 화면에 그리기 전에 수행되므로 DOM에 영향을 미치는 코드를 넣어도 화면 깜빡임이 발생하지 않는다.
하지만 동기적인 동작에서 발생하는 단점을 가지고 있다. 만약 useLayoutEffect에서 오랜 시간이 소요된다고 가정하면 유저는 그 시간 동안 빈 화면을 보게 되는 것이다.
그렇기에 useEffect를 사용하여 설계하다가 UX적으로 문제가 발생하면 useLayoutEffect를 적절히 섞어서 사용하는 것이 좋다.
useLayoutEffect 문제점
SSR 환경에서 사용할 경우 에러가 발생한다.
이를 수정하기 위해서는 로직을 useEffect로 이동하거나 클라이언트 렌더링이 완료될 때까지 컴포넌트 노출을 지연하는 방법이 있다.
서버에서 렌더링된 HTML에서 layoutEffect가 필요한 컴포넌트를 배제하고 싶다면, showChild && <Child />
를 사용하여 조건적으로 렌더링 하고 useEffect(() => { setShowChild(true); }, [])
를 사용하여 노출을 지연시킬 수 있다.
이런 방법으로 JS 코드가 주입되기 전에 깨져 보일 수 있는 UI는 표현되지 않게 된다.
SSR에서도 사용하고 싶은 경우 렌더링이 되어 window 객체가 생기는지에 따라 useEffect를 사용할지 useLayoutEffect를 사용할지 결정할 수 있다.
// 커스텀 훅
import { useEffect, useLayoutEffect } from "react";
export const useIsomorphicEffect = () => {
return typeof window !== "undefined" ? useLayoutEffect : useEffect;
};
// 커스텀 훅 사용
import { useEffect, useLayoutEffect } from "react";
import { useIsomorphicEffect } from "./useIsomorphicEffect";
const App = () => {
const isomorphicEffect = useIsomorphicEffect();
isomorphicEffect(() => {
// do something you want
}, []);
return <div>Hellow world</div>;
};
커스텀 훅으로 만들어 두어 사용할 부분에서 Effect로 사용하면 된다.
0406
캐시
웹 캐시는 자주 쓰이는 문서의 사본을 자동으로 보관하는 HTTP 장치다.
웹 요청이 캐시에 도착했을 때, 캐시된 로컬 사본이 존재한다면, 그 문서는 원 서버가 아니라 캐시로부터 제공된다.
- 캐시는 불필요한 데이터 전송을 줄여서, 네트워크 요금 비용을 줄여 준다.
- 캐시는 네트워크 병목을 줄여서 대역폭을 늘리지 않고도 페이지를 빨리 불러올 수 있게 된다.
- 캐시는 원 서버에 대한 요청을 줄여준다. 서버는 부하를 줄일 수 있으며 더 빨리 응답할 수 있게 된다.
- 페이지를 먼 곳에서 불러올수록 시간이 많이 걸리는데, 캐시는 거리로 인한 지연을 줄여준다.
캐시가 없을 때 문제점
불필요한 데이터 전송
복수의 클라이언트가 자주 쓰이는 원 서버 페이지에 접근할 때, 서버는 같은 문서를 클라이언트들에게 각각 한 번씩 전송하게 된다.
똑같은 바이트들이 네트워크를 통해 계속 반복해서 이동한다. 이 불필요한 데이터 전송은 값비싼 네트워크 대역폭을 잡아먹고, 전송을 느리게 만들며, 웹 서버에 부하를 준다.
캐시를 이용하면, 첫 번째 서버 응답은 캐시에 보관된다. 캐시된 사본이 뒤이은 요청들에 대한 응답으로 사용될 수 있기 때문에, 원 서버가 중복해서 트래픽을 주고받는 낭비가 줄어들게 된다.
대역폭 병목
캐시는 네트워크 병목을 줄여준다. 많은 네트워크가 원격 서버보다 로컬 네트워크 클라이언트에 더 넓은 대역폭을 제공한다.
만약 클라이언트가 빠른 LAN에 있는 캐시로부터 사본을 가져온다면, 캐싱은 성능을 대폭 개선할 수 있을 것이다.
대역폭은 네트워크 속도와 문서 크기에 따라 전송 시간에 영향을 미친다.
큰 문서에 대해 현저한 지연을 일으키며, 속도는 네트워크 종류의 차이에 따라 극적으로 달라진다.
갑작스런 요청 쇄도(Flash Crowds)
갑작스런 사건(뉴스 속보, 이벤트 등)으로 인해 많은 사람이 동시에 웹 문서에 접근할 때 이런 일이 발생한다.
이 결과로 초래된 불필요한 트래픽 급증은 네트워크와 웹 서버의 심각한 장애를 야기시킨다.
캐싱은 이에 대처하기 위해 특히 중요하다.
거리로 인한 지연
대역폭이 문제가 되지 않더라도, 거리가 문제가 될 수 있다.
모든 네트워크 라우터는 제각각 인터넷 트래픽을 지연시킨다. 그리고 클라이언트와 서버 사이에 라우터가 그다지 많지 않더라도 유의미한 지연을 유발한다.
기계실 근처에 캐시를 설치해서 문서가 전송되는 거리를 수천 킬로키터에서 수십 미터로 줄일 수 있다.
적중과 부적중
캐시가 세상 모든 문서의 사본을 저장하지는 않는다.
캐시에 요청이 도착했을 때, 대응하는 사본이 있다면 이를 이용해 요청이 처리될 수 있다. 이것을 캐시 적중(cache hit)이라고 부른다.
만약 대응하는 사본이 없다면 원 서버로 전달되기만 할 뿐이다. 이것을 캐시 부적중(cache miss)이라고 부른다.
재검사
원서버 콘텐츠는 변경될 수 있기 때문에, 캐시는 반드시 사본이 여전히 최신인지 서버를 통해 때때로 점검해야 한다.
이러한 ‘신선도 검사’를 HTTP 재검사라 부른다.
효과적인 재검사를 위해, HTTP는 서버로부터 전체 객체를 가져오지 않고도 콘텐츠가 여전히 신선한지 빠르게 검사할 수 있는 특별한 요청을 정의했다.
대부분의 캐시는 클라이언트가 사본을 요청하였으며 그 사본이 검사가 필요가 있을 정도로 충분히 오래된 경우에만 재검사를 한다.
캐시는 재검사가 필요할 때, 원서버에 작은 재검사 요청을 보낸다. 콘텐츠가 변경되지 않았다면, 서버는 304 응답을 보낸다. 사본이 유효함을 알게 된 캐시는 사본을 임시로 다시 표시한 뒤 클라이언트에게 제공한다.
이는 순수 캐시 적중보다는 느린데, 원 서버와 검사를 할 필요가 있기 때문이다.
하지만 캐시 부적중보다는 빠른데, 서버로부터 객체 데이터를 받아올 필요가 없기 때문이다.
이때 가장 많이 쓰이는 것은 If-Modified-Since 헤더다. 서버에게 보내는 GET 요청에 이 헤더를 추가하면 캐시된 시간 이후 변경된 경우에만 사본을 보내달라는 의미가 된다.
재검사 적중
만약 서버 객체가 변경되지 않았다면, 서버는 클라이언트에게 304 응답을 보낸다.
재검사 부적중
만약 서버 객체가 캐시된 사본과 다르다면, 서버는 콘텐츠 전체와 함께 평범한 200 응답을 클라이언트에게 보낸다.
객체 삭제
만약 서버 객체가 삭제되었다면, 서버는 404 응답을 돌려보내며, 캐시는 사본을 삭제한다.
적중률
캐시가 요청을 처리하는 비율을 캐시 적중률, 혹은 문서 적중률이라고 부른다.
0%는 모든 요청이 캐시 부적중, 100%는 모든 요청이 캐시 적중임을 의미한다.
문서들이 모두 같은 크기인 것은 아니기 때문에 문서 적중률이 모든 것을 말해주지 않는다.
몇몇 큰 객체는 덜 접근되지만 그 크기 때문에 전체 트래픽에는 더 크게 기여한다.
바이트 단위 적중률은 캐시를 통해 제공된 모든 바이트의 비율을 표현한다.
문서 적중률을 개선하면 전체 대기시간이 줄어들고, 바이트 단위 적중률을 개선하면 대역폭 절약을 최적화할 수 있다.
적중과 부적중의 구별
HTTP는 클라이언트에게 응답이 캐시 적중이었는지 아니면 원 서버 접근인지 말해줄 수 있는 방법을 제공하지 않는다.
두 경우 모두 응답 코드는 응답이 본문을 갖고 있음을 의미하는 200이 될 것이다.
어떤 사용 프락시 캐시는 캐시에 무슨 일이 일어났는지 설명하기 위해 Via 헤더에 추가 정보를 붙인다.
클라이언트가 응답이 캐시에서 왔는지 알아내는 한 가지 방법은 Date 헤더를 이용하는 것이다.
응답의 Date 헤더 값을 현재 시각과 비교하여, 응답 생성일이 더 오래되었다면 클라이언트는 응답이 캐시된 것임을 알아낼 수 있다.
캐시 토폴로지
캐시는 한 명의 사용자에게만 할당될 수도 있고, 반대로 수천 명의 사용자들 간에 공유될 수도 있다.
개인 전용 캐시
개인 전용 캐시는 많은 에너지나 저장 공간을 필요로 하지 않으므로, 작고 저렴할 수 있다.
웹브라우저는 개인 전용 캐시를 내장하고 있다.
대부분의 브라우저는 자주 쓰이는 문서를 개인용 컴퓨터의 디스크와 메모리에 캐시해 놓고, 사용자가 캐시 사이즈와 설정을 수정할 수 있도록 허용한다.
공용 프락시 캐시
공용 캐시는 캐시 프락시 서버 혹은 프락시 캐시라고 불리는 특별한 종류의 공유된 프락시 서버이다.
프락시 캐시는 로컬 캐시에서 문서를 제공하거나, 혹은 사용자의 입장에서 서버에 접근한다.
공용 캐시에서, 캐시는 자주 찾는 객체를 단 한 번만 가져와 모든 요청에 대해 공유된 사본을 제공함으로써 네트워크 트래픽을 줄인다.
프락시 캐시 계층들
작은 캐시에서 캐시 부적중이 발생했을 때 더 큰 부모 캐시가 남겨진 트래픽을 처리하도록 하는 계층을 만드는 방식이 합리적인 경우가 많다.
클라이언트 주위에는 작고 저렴한 캐시를 사용하고, 계층 상단에는 많은 사용자들에 의해 공유되는 문서를 유지하기 위해 더 크고 강력한 캐시를 사용하자는 것이다.
캐시 계층이 깊다면 요청은 캐시의 긴 연쇄를 따라가게 될 것이다. 프락시 연쇄가 깊어질수록 각 중간 프락시는 성능 저하가 발생할 것이다.
캐시망, 콘텐츠 라우팅, 피어링
몇몇 네트워크 아키텍처는 단순한 캐시 계층 대신 복잡한 캐시망을 만든다.
캐시망의 프락시 캐시는 복잡한 방법으로 대화하여, 어떤 부모 캐시와 대화할 것인지, 아니면 원 서버로 바로 가도록 할 것인지에 대한 결정을 내린다.
이러한 캐시 사이 관계는, 서로 다른 조직들이 상호 이득을 위해 그들의 캐시를 연결하여 서로를 찾아볼 수 있도록 해준다.
선택적인 피어링을 지원하는 캐시는 형제 캐시라고 불린다.
HTTP는 형제 캐시를 지원하지 않기에, ICP나 HTCP 같은 프로토콜을 이용해 HTTP를 확장했다.
캐시 처리 단계
- 요청 받기 - 캐시는 네트워크로부터 도착한 요청 메시지를 읽는다.
- 파싱 - 캐시는 메시지를 파싱하여 URL과 헤더들을 추출한다.
- 검색 - 캐시는 로컬 복사본이 있는지 검사하고, 사본이 없다면 사본을 받아온다.(그리고 로컬에 저장한다.)
- 신선도 검사 - 캐시는 캐시된 사본이 충분히 신선한지 검사하고, 신선하지 않으면 변경사항이 있는지 서버에게 물어본다.
- 응답 생성 - 캐시는 새로운 헤더와 캐시된 본문으로 응답 메시지를 만든다.
- 발송 - 캐시는 네트워크를 통해 응답을 클라이언트에게 돌려준다.
- 로깅 - 선택적으로, 캐시는 로그파일에 트랜잭션에 대해 서술한 로그 하나를 남긴다.
사본을 신선하게 유지하기
오래된 데이터를 제공하는 캐시는 불필요하기에 캐시된 데이터는 서버의 데이터와 일치하도록 관리되어야 한다.
HTTP는 어떤 캐시가 사본을 갖고 있는지 서버가 기억하지 않더라도, 캐시된 사본이 서버와 충분히 일치하도록 유지할 수 있게 해주는 메커니즘을 갖고 있다. 이를 문서 만료와 서버 재검사라고 부른다.
문서 만료
HTTP는 Cache-Control과 Expires라는 특별한 헤더들을 이용해서 원 서버가 각 문서에 유효기간을 붙일 수 있게 해준다.
캐시 문서가 만료되기 전에, 캐시는 필요하다면 서버와의 접촉 없이 사본을 제공할 수 있다.
문서가 만료되면, 캐시는 반드시 서버와 문서에 변경된 것이 있는지 검사해야 하며, 만약 그렇다면 신선한 사본을 유효기간과 함께 얻어와야 한다.
유효기간과 나이
서버는 응답 본문과 함께 하는, HTTP/1.0 Expires나 HTTP/1.1 Cache-Control: max-age 응답 헤더를 이용해서 유효기간을 명시한다.
둘 다 같은 일을 하지만, 절대 시간은 컴퓨터의 시계가 올바르게 맞추어져 있을 것을 요구한다.
서버 재검사
캐시된 문서가 만료되었다는 것은, 그 문서가 원 서버에 현재 존재하는 것과 실제로 다르다는 것을 의미하지는 않으며, 다만 이제 검사할 시간이 되었음을 뜻한다. 이 검사를 서버 재검사라고 부른다.
- 재검사 결과 콘텐츠가 변경되었다면, 캐시는 그 문서의 새로운 사본을 가져와 오래된 데이터 대신 저장한 뒤 클라이언트에게도 보내준다.
- 변경되지 않았다면, 캐시는 새 만료일을 포함한 새 헤더들만 가져와서 캐시 안의 헤더들을 갱신한다.
조건부 메서드와의 재검사
HTTP 조건부 메서드는 재검사를 효율적으로 만들어준다.
조건부 GET은 GET 요청 메시지에 특별한 조건부 헤더를 추가함으로써 시작된다.
웹 서버는 조건이 참인 경우에만 객체를 반환한다.
조건부 요청 헤더 중 가장 유용한 것은 If-Modified-Since와 If-None-Match이다.
If-Modified-Since: 날짜 재검사
만약 문서가 주어진 날짜 이후로 수정되었다면 요청 메서드를 처리한다.
If-Modified-Since 헤더는 서버 응답 헤더의 Last-Modified 헤더와 함께 동작한다.
원 서버는 제공하는 문서에 최근 변경 일시를 붙인다. 캐시가 캐시된 문서를 재검사 하려고 할 때, 캐시된 사본이 마지막으로 수정된 날짜가 담긴 If-Modified-Since 헤더를 포함한다.
If-Modified-Since: <캐시된 마지막 수정일>
만약 콘텐츠가 그동안 변경되었다면, 최근 변경 일시는 다를 것이다.
If-None-Match: 엔터티 태그 재검사
최근 변경 일시 재검사가 적절히 행해지기 어려운 상황이 몇 가지 있다.
- 어떤 문서는 일정 시간 간격으로 다시 쓰여지지만 실제로는 같은 데이터를 포함하고 있다. 내용에는 아무런 변화가 없더라도 변경시각은 바뀔 수 있다.
- 어떤 문서들의 변경은 전 세계의 캐시들이 그 데이터를 다시 읽어들이기엔 사소한 것일 수 있다.
- 어떤 서버들은 그들이 갖고 있는 페이지에 대한 최근 변경 일시를 정확하게 판별할 수 없다.
- 1초보다 작은 간격으로 갱신되는 문서를 제공하는 서버들에게는, 변경일에 대한 1초의 정밀도는 충분하지 않을 수 있다.
퍼블리셔가 문서를 변경했을 때, 그는 문서의 엔터티 태그를 새로운 버전으로 표현할 수 있다.
엔터티 태그가 변경되었다면, 캐시는 새 문서의 사본을 얻기 위해 If-None-Match 조건부 헤더를 사용할 수 있다.
만약 서버의 엔터티 태그가 변경되었다면, 서버는 200 응답으로 새 콘텐츠를 새 ETag와 함께 반환했을 것이다.
약한 검사기와 강한 검사기
엔터티 태그와 최근 변경일시는 둘 다 캐시 검사기다.
서버는 때때로 모든 캐시된 사본을 무효화시키지 않고 문서를 살짝 고칠 수 있도록 허용하고 싶은 경우가 있다.
HTTP/1.1은, 비록 콘텐츠가 조금 변경되었더라도 그 정도면 같은 것이라고 서버가 주장할 수 있도록 약한 검사기를 지원한다.
강한 검사기는 콘텐츠가 바뀔 때마다 바뀐다.
서버는 ‘W/’ 접두사로 약한 검사기를 구분한다.
언제 엔터티 태그를 사용하고 언제 Last-Modified 일시를 사용하는가
만약 서버가 엔터티 태그를 반환했다면, 반드시 엔터티 태그 검사기를 사용해야 한다.
만약 서버가 Last-Modified 값만을 반환했다면, If-Modified-Since 검사를 사용할 수 있다.
만약 엔터티 태그와 최근 변경 일시가 모두 사용 가능하다면, 각각을 위해 두 가지의 재검사 정책을 모두 사용해야 한다.
0407
DOM 계층 구조 이해하기
타입스크립트에서 DOM 엘리먼트의 계층 구조를 파악하기 용이하다.
Element와 EventTarget에 달려 있는 Node의 구체적인 타입을 안다면 타입 오류를 디버깅할 수 있고, 언제 타입 단언을 사용해야 할지 알 수 있다.
DOM에 접근하는 코드를 작성하면 JS에서는 문제가 발생하지 않지만, 타입스크립트에서는 수많은 오류가 표시된다.
EventTarget 타입 관련 오류
예를 들어, HTMLParagraphElement는 HTMLElement의 서브타입이고, HTMLElement는 Element의 서브타입이다. 또한 Element는 Node의 서브타입이고, Node는 EventTarget의 서브타입이다.
EventTarget
EventTarget은 DOM 타입 중 가장 추상화된 타입이다.
이벤트 리스너를 추가하거나 제거하고, 이벤트를 보내는 것밖에 할 수 없다.
function handleDrag(eDown: Event) {
const targetEl = eDown.currentTarget;
targetEl.classList.add("dragging");
// 개체가 'null'인 것 같습니다.
// 'EventTarget' 형식에 'classList' 속성이 없습니다.
}
- Event의 currentTarget 속성의 타입은
EventTarget | null
이다. 그러기 때문에 null 가능성이 오류로 표시되었다. - EventTarget 타입에 classList 속성이 없기 때문에 오류가 되었다.
Node
예시로 document나 텍스트 노드, 주석이 있다.
childNodes 프로퍼티는 엘리먼트뿐만 아니라 텍스트 노드와 주석까지도 포함하고 있다.
Element, HTMLElement
SVG 태그의 전체 계층 구조를 포함하면서 HTML이 아닌 엘리먼트가 존재하는데, 바로 Element의 또 다른 종류인 SVGElement이다.
에를 들어, <html>
은 HTMLHtmlElement이고, <svg>
는 SVGSvgElement이다.
HTMLxxxElement
특정 엘리먼트들은 자신만의 고유한 속성을 가지고 있다.
예를 들어, HTMLImageElement에는 src 속성이 있고, HTMLInputElement에는 value 속성이 있다.
이런 속성에 접근하려면, 타입 정보 역시 실제 엘리먼트 타입이어야 하므로 상당히 구체적으로 타입을 지정해야 한다.
항상 정확한 타입을 얻을 수 있는 것은 아니다. 특히 document.getElementById
에서 문제가 발생하게 된다.
document.getElementById('my-div'); // HTMLElement
일반적으로 타입 단언문은 지양해야 하지만, DOM 관련해서는 타입스크립트보다 개발자가 더 정확히 알고 있는 경우이므로 단언문을 사용해도 좋다.
document.getElementById('my-div') as HTMLDivElement;
strictNullChecks가 설정된 상태라면, null인 경우를 체크해야 한다. 실제 코드에서 null일 가능성이 있다면 if 분기문을 추가해야 한다.
document.getElementById('my-div')!;
Event 타입 관련 오류
function handleDrag(eDown: Event) {
const dragStart = [eDown.clientX, eDown.clientY];
// 'Event' 형식에 'clientX' 속성이 없습니다.
// 'Event' 형식에 'clientY' 속성이 없습니다.
}
- EventTarget 타입의 계층 구조뿐 아니라, Event 타입에도 별도의 계층 구조가 있다.
- Event는 가장 추상화된 이벤트이다. 더 구체적인 타입들은 다음과 같다.
- UIEvent: 모든 종류의 사용자 인터페이스 이벤트
- MouseEvent: 클릭처럼 마우스로부터 발생되는 이벤트
- KeyboardEvent: 키 누름 이벤트
- handleDrag 함수의 매개변수는 Event로 선언된 반면 clientX와 clientY는 보다 구체적인 MouseEvent 타입에 있기 때문에 오류가 발생한 것이다.
- ‘mousedown’ 이벤트 핸들러를 인라인 함수로 만들면 타입스크립트는 더 많은 문맥 정보를 사용하게 되고, 대부분의 오류를 제거할 수 있다.
- 또한 매개변수 타입을 Event 대신 MouseEvent로 선언할 수 있다.
정보를 감추는 목적으로 private 사용하지 않기
자바스크립트는 클래스에 비공개 속성을 만들 수 없다.
많은 이가 비공개 속성임을 나타내기 위해 언더스코어(_
)를 접두사로 붙이던 것이 관례로 인정될 뿐이었다.
그러나 속성에 언더스코어를 붙이는 것은 단순히 비공개라고 표시한 것뿐이다.
따라서 일반적인 속성과 동일하게 클래스 외부로 공개되어 있다는 점을 주의해야 한다.
타입스크립트에는 public, protected, private 접근 제어자를 사용해서 공개 규칙을 강제할 수 있는 것으로 오해할 수 있다.
그러나 접근 제어자는 타입스크립트 키워드이기 때문에 컴파일 후에는 제거된다.
그렇기에 컴파일 시점에만 오류를 표시해 줄 뿐이며, 런타임에는 아무런 효력이 없다.
즉, 정보를 숨기기 위해 private을 사용하면 안 된다. 정보를 숨기기 위해 가장 효과적인 방법은 클로저를 사용하는 것이다.
단 숨기기 위한 정보에 접근해야 하는 메서드 역시 생성자 내부에 정의되어야 하기에 인스턴스를 생성할 때마다 메서드의 복사본이 생성되어 메모리를 낭비한다.
또 하나의 선택지로, 비공개 필드 기능을 사용할 수 있다. 비공개 필드 기능은 접두사로 #
를 붙여서 타입 체크와 런타임 모두에서 비공개로 만드는 역할을 한다.
소스맵을 사용하여 타입스크립트 디버깅하기
타입스크립트 코드를 실행한다는 것은, 엄밀히 말하자면 타입스크립트 컴파일러가 생성한 자바스크립트 코드를 실행한다는 것이다.
컴파일러, 전처리기, 압축기 등을 통해 변환된 JS 코드는 복잡해 디버깅하기 매우 어렵다.
디버깅 문제를 해결하기 위해 소스맵이라는 해결책이 등장했다.
소스맵은 변환된 코드의 위치와 심벌들을 원본 코드의 원래 위치와 심벌들로 맵핑한다.
{
"compilerOptions": {
"sourceMap": true
}
}
컴파일을 실행하면 각 .ts 파일에 대해 .js와 .js.map 두 개의 파일을 생성한다. .js.map 파일이 바로 소스맵이다.
0408
모던 자바스크립트로 작성하기
58장부터는 타입스크립트로 마이그레이션 하는 데에 필요한 조언들을 배울 수 있었다.
타입스크립트는 코드를 특정 버전의 자바스크립트로 컴파일하는 기능도 가지고 있기에 트랜스파일러로 사용할 수 있다.
- ECMAScript 모듈 사용하기
- 프로토타입 대신 클래스 사용하기
- var 대신 let/const 사용하기
- for(;;) 대신 for-of 또는 배열 메서드 사용하기
- 함수 표현식보다 화살표 함수 사용하기
- 단축 객체 표현과 구조 분해 할당 사용하기
- 함수 매개변수 기본값 사용하기
- 저수준 프로미스나 콜백 대신 async/await 사용하기
- 연관 배열에 객체 대신 Map과 Set 사용하기
- 타입스크립트에 use strict 넣지 않기
타입스크립트 도입 전에 @ts-check와 JSDoc으로 시험해 보기
본격적으로 타입스크립트로 전환하기에 앞서, @ts-check 지시자를 사용하면 타입스크립트 전환시에 어떤 문제가 발생하는지 미리 시험해 볼 수 있다.
@ts-check 지시자를 사용하여 타입 체커가 파일을 분석하고, 발견된 오류를 보고하도록 지시한다.
주의할 점은 @ts-check 지시자는 매우 느슨한 수준으로 타입 체크를 수행한다는 것이다.
선언되지 않은 전역 변수
어딘가에 숨어 있는 변수라면, 변수를 제대로 인식할 수 있게 별도의 타입 선언 파일을 만들어야 한다.
types.d.ts 파일을 만든 뒤 declare 키워드를 활용하여 변수를 선언하면 오류가 해결된다.
알 수 없는 라이브러리
서드파티 라이브러리를 사용하는 경우, 서드파티 라이브러리의 타입 정보를 알아야 한다.
예를 들어, 제이쿼리를 사용하면 @ts-check 지시자를 사용하면 제이쿼리를 사용한 부분에서 오류가 발생한다.
제이쿼리 타입 선언을 설치하면 제이쿼리의 사양 정보를 참조하게 된다.
DOM 문제
JSDoc을 사용하여 타입 단언을 대체할 수 있다.
// @ts-check
const ageEl = /** @type {HTMLInputElement} */ (document.getElementById("age"));
ageEl.value = "12";
부정확한 JSDoc
프로젝트에 이미 JSDoc 스타일의 주석을 사용 중이었다면, @ts-check 지시자를 설정하는 순간부터 기존 주석에 타입 체크가 동작하게 되고 갑자기 수많은 오류가 발생하게 될 것이다.
이때는 당황하지 말고 타입 정보를 차근차근 추가해 나가면 된다.
allowJs로 타입스크립트와 자바스크립트 같이 사용하기
- 점진적 마이그레이션을 위해 JS와 TS를 동시에 사용할 수 있게 allowJs 컴파일러 옵션을 사용한다.
- 대규모 마이그레이션 작업을 시작하기 전에, 테스트와 빌드 체인에 타입스크립트를 적용해야 한다.
의존성 관계에 따라 모듈 단위로 전환하기
점진적 마이그레이션을 할 때는 모듈 단위로 각개격파하는 것이 이상적이다.
그런데 한 모듈을 골라서 타입 정보를 추가하면, 해당 모듈이 의존하는 모듈에서 비롯되는 타입 오류가 발생하게 된다.
의존성과 관련된 오류 없이 작업하려면, 다른 모듈에 의존하지 않는 최하단 모듈부터 작업을 시작해서 의존성의 최상단에 있는 모듈을 마지막으로 완성해야 한다.
프로젝트 내에 존재하는 모듈은 서드파티 라이브러리에 의존하지만 서드파티 라이브러리는 해당 모듈에 의존하지 않기 때문에, 서드파티 라이브러리 타입 정보를 가장 먼저 해결해야 한다.
일반적으로 @types 모듈을 설치하면 된다.
외부 API를 호출하는 경우도 있기 때문에 외부 API의 타입 정보도 추가해야 한다.
마이그레이션할 때는 타입 정보 추가만 하고, 리팩터링을 해서는 안 된다.
선언되지 않은 클래스 멤버
자바스크립트는 클래스 멤버 변수를 선언할 필요가 없지만, 타입스크립트에서는 명시적으로 선언해야 한다.
이는 빠른 수정 기능으로 간단히 해결할 수 있다.
빠른 수정을 적용한 후에 속성을 훑어보고 any로 추론된 부분을 직접 수정해야 한다.
잘못된 설계를 발견했을 때, 잘못된 설계 그대로 타입스크립트로 전환하는 것은 납득하기 어려운 일이다. 하지만 리팩터링을 하면 안 된다.
타입이 바뀌는 값
객체에 프로퍼티를 추가하는 코드는 JS에서는 문제 없지만, TS에서는 문제가 발생한다.
이때 한 번에 객체를 생성하면 오류를 해결할 수 있다.
한꺼번에 생성하기 곤란한 경우에는 임시 방편으로 타입 단언문을 사용할 수 있다. 마이그레이션이 완료된 이후에는 타입 선언문을 사용하여 문제를 제대로 해결하자.
마지막 단계로, 테스트 코드를 타입스크립트로 전환하면 된다.
로직 코드가 테스트 코드에 의존하지 않기 때문에, 테스트 코드는 항상 의존성 관계도의 최상단에 위치하며 마이그레이션의 마지막 단계가 되는 것은 자연스러운 일이다.
그리고 최하단의 모듈부터 TS로 전환하는 와중에도 테스트 코드는 변경되지 않았고, 테스트를 수행할 수 있었을 것이다.
마이그레이션 기간 중에 테스트를 수행할 수 있다는 것은 엄청난 이점이다.
마이그레이션의 완성을 위해 noImplicitAny 설정하기
프로젝트 전체를 .ts로 전환한 뒤, 마지막 단계는 noImplicitAny를 설정하는 것이다.
noImplicitAny가 설정되지 않은 상태에서는 타입 선언에서 비롯되는 실제 오류가 숨어 있기 때문에 마이그레이션이 완료되었다고 할 수 없다.
처음에는 noImplicitAny를 로컬에만 설정하고 작업하는 것이 좋다.
왜냐하면 원격에서는 설정에 변화가 없기 때문에 빌드가 실패하지 않기 때문이다.
로컬에서만 오류로 인식되기 때문에, 수정된 부분만 커밋할 수 있어서 점진적 마이그레이션이 가능하다.
엄격한 타입 체크를 적용하기 전에 팀원들이 타입스크립트에 익숙해질 수 있도록 타입 체크 설정을 점진적으로 적용해야 한다.