0124 - 0130
0124
타입스크립트란?
자바스크립트에 타입을 부여한 언어
자바스크립트의 확장된 언어로 실행하려면 컴파일 과정을 거쳐야 한다.
타입스크립트 장점
에러를 사전에 방지하여 코드의 품질을 높이고, 코드 자동 완성 기능을 최대로 활용하고 가이드 역할을 통해 개발 생산성을 높일 수 있다.
자바스크립트를 타입스크립트처럼 코딩하는 방법
// @ts-check
/**
* @param {number} a 첫번째 숫자
* @param {number} b 두번째 숫자
*/
function sum(a, b) {
return a + b;
}
- JSDoc의 기능과
@ts-check주석을 활용하여 자바스크립트를 타입스크립트처럼 코딩할 수 있다. - 다만 이렇게 하면 코드의 양이 많아지고 불편하다는 단점이 있다.
타입스크립트 설치와 사용
npm i typescript -g- 타입스크립트를 전역 설치한다.
- 타입스크립트로 작성된 코드는 브라우저가 해석할 수 없기 때문에 JS 파일로 컴파일 해줘야 한다.
tsc 변환할파일- ts 확장자를 가진 파일을 js 파일로 컴파일한다.
타입스크립트 설정 파일
tsconfig.json 파일을 생성하고 키 값 형태로 작성한다.
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"noImplicitAny": true
},
"include": ["./src/**/*"]
}
- compilerOptions는 컴파일 할 때의 옵션을 설정한다.
- allowJs는 프로젝트 내에 자바스크립트를 허용하겠다는 의미
- checkJs는
@ts-check의 기능과 동일하다. - noImplicitAny는 모든 타입에 대해서 최소 Any라고 하는 기본적인 타입이라도 작성해야한다는 의미
타입스크립트 플레이그라운드
위의 사이트에서 타입스크립트로 작성한 코드를 컴파일한 결과를 확인해 볼 수 있다.
reference
타입스크립트 공식 문서
타입스크립트 핸드북
타입스크립트 입문 - 기초부터 실전까지
0125
변수와 함수 타입 정의
기본 타입
String
자바스크립트 변수 타입이 문자열인 경우 아래와 같이 선언해서 사용한다.
let str: string = "hi";
- 위와 같이
:을 이용하여 자바스크립트 코드에 타입을 정의하는 방식을 타입 표기라고 한다.
Number
let num: number = 10;
Boolean
let isLoggedIn: boolean = false;
Array
let arr: number[] = [1, 2, 3];
let arr: Array<number> = [1, 2, 3];
- 배열의 경우에는 두 가지 방법으로 선언할 수 있다.
- 배열 리터럴 앞에 배열에 포함될 값을 명시하거나, 제네릭을 사용하여 선언할 수 있다.
Tuple
튜플은 배열의 길이가 고정되고 각 인덱스에 위치한 타입이 지정되어 있는 배열 형식을 의미한다.
let address: [string, number] = ["gangnam", 100];
Object
let person: { name: string; age: number } = {
name: "thor",
age: 1000,
};
Any
기존 자바스크립트로 구현되어 있는 웹 서비스 코드에 타입스크립트를 점진적으로 적용할 때 활용하면 좋은 타입
단어 의미 그대로 모든 타입에 대해서 허용
let str: any = "hi";
let num: any = 10;
let arr: any = ["a", 2, true];
Void
변수에는 undefined와 null만 할당하고, 함수에는 반환 값을 설정할 수 없는 타입
let unuseful: void = undefined;
function notuse(): void {
console.log("sth");
}
함수 타입
파라미터 타입 정의
function sum(a: number, b: number) {
return a + b;
}
- 타입스크립트에서는 함수의 인자를 모두 필수 값으로 간주하고, 추가로 인자를 넘기면 오류를 발생시킨다.
반환값 타입 정의
function getTen(): number {
return 10;
}
- 반환값의 경우 함수명과 매개변수 뒤에 작성한다.
- 적절한 값을 반환하지 않으면 컴파일러가 오류를 검출한다.
옵셔널 파라미터
매개변수의 갯수 만큼 인자를 넘기지 않아도 되는 자바스크립트의 특성을 살리기 위해서는 ?를 이용해서 아래와 같이 정의할 수 있다.
function sum(a: number, b?: number): number {
return a + b;
}
reference
타입스크립트 공식 문서
타입스크립트 핸드북
타입스크립트 입문 - 기초부터 실전까지
0126
인터페이스 & 타입 별칭
할일 관리 애플리케이션 실습을 진행하며 공부한 타입스크립트 기본 적용을 시도해보았다.
타입을 일일히 작성하며 느낀 점은 객체의 경우 매번 중복해서 작성하는 데에 불편함을 느꼈고, 코드의 가독성에도 좋지 않다는 생각이 들었다.
이 문제를 해결하기 위해 인터페이스라는 개념에 대해 학습해보았다.
인터페이스란?
인터페이스는 상호 간에 정의한 약속 혹은 규칙을 의미한다. 타입스크립트에서의 인터페이스는 보통 다음과 같은 범주에 대해 약속을 정의할 수 있다.
- 객체의 스펙(속성과 속성의 타입)
- 함수의 파라미터
- 함수의 스펙(파라미터, 반환 타입 등)
- 배열과 객체를 접근하는 방식
- 클래스
interface User {
age: number;
name: string;
}
let junghoon: User = {
age: 27,
name: "정훈",
};
함수에 인터페이스 적용
function getUser(user: User) {
console.log(user);
}
const capt = {
name: "캡틴",
age: 100,
};
getUser(capt);
- 파라미터에 인터페이스를 지정하고, 함수를 호출할 때 인자가 인터페이스의 규칙을 따르는지 확인해준다.
- 인터페이스를 인자로 받아 사용할 때, 인터페이스에 정의된 속성과 타입의 조건만 만족한다면 객체의 속성 갯수가 더 많아도 상관 없다.
함수의 스펙을 인터페이스로 정의
interface SumFunction {
(a: number, b: number): number;
}
let sum: SumFunction;
sum = function (a: number, b: number): number {
return a + b;
};
- 라이브러리를 직접 만들거나 협업을 할 때 틀을 잡는 용도로 활용 가능
인덱싱 방식 정의
interface StringArray {
[index: number]: string;
}
const arr: StringArray = ["a", "b", "c"];
arr[0] = 10; // 에러
- 배열에 인덱스로 접근했을 때 요소의 값은 모두 문자열이라는 것을 정의한다.
- 즉, 모든 요소는 문자열 타입이다.
딕셔너리 패턴
위의 인덱싱 방식과 비슷한 패턴이다
interface StringRegexDictionary {
[key: string]: RegExp;
}
var obj: StringRegexDictionary = {
sth: /abc/,
cssFile: /\.css$/,
jsFile: /\.js$/,
};
obj["cssFile"] = "a";
- 값으로 오는 모든 값들은 정규표현식 형태여야 한다.
인터페이스 확장(상속)
인터페이스는 다른 인터페이스를 상속받아 확장이 가능하다.
interface Person {
name: string;
age: number;
}
interface Developer extends Person {
language: string;
}
const junghoon: Developer = {
language: "ts",
age: 27,
name: "jh",
};
타입 별칭
타입 별칭은 특정 타입이나 인터페이스를 참조할 수 있는 타입 변수를 의미한다.
type myName = string;
const name: MyName = "capt";
type Developer = {
name: string;
skill: string;
};
타입 별칭의 특징
타입 별칭은 타입 값을 생성하는 것이 아니라 정의한 타입에 대해 나중에 쉽게 참고할 수 있게 이름을 부여하는 것과 같다.
인터페이스로 선언한 타입을 프리뷰 했을 때는 interface 명만 나타나는 반면,
타입 별칭을 사용해서 선언한 타입을 프리뷰로 확인했을 때는 속성과 그 속성의 타입까지 확인할 수 있다.
type과 interface의 차이점
- 타입 별칭과 인터페이스의 가장 큰 차이점은 타입의 확장 가능 / 불가능 여부이다.
- 인터페이스는 확장이 가능한데 반해 타입 별칭은 확장이 불가능하기에 가능한 type 보다 interface로 선언해서 사용하는 것이 좋다.
reference
타입스크립트 공식 문서
타입스크립트 핸드북
타입스크립트 입문 - 기초부터 실전까지
0127
연산자를 이용한 타입 정의
Union Type
유니온 타입은 자바스크립트 OR 연산자 ||와 같이 A이거나 B이다 라는 의미의 타입이다.
function logText(text: string | number) {}
- 위 함수의 파라미터 text에는 숫자나 문자 타입의 값이 모두 올 수 있다.
- 이처럼
|연산자를 이용하여 타입을 여러 개 연결하는 방식을 유니온 타입 정의 방식이라고 부른다.
장점
function logMessage(value: string | number) {
if (typeof value === "number") {
value.toLocaleString();
}
if (typeof value === "string") {
value.toString();
}
throw new TypeError("value must be string or number");
}
- 위와 같이 코드를 작성하면 value의 타입이 추론되기 때문에 타입과 관련된 API를 쉽게 자동완성할 수 있다.
- 위와 같이 특정 타입으로 타입의 범위를 좁혀가는 과정을 타입 가드라고 한다.
특징
interface Developer {
name: string;
skill: string;
}
interface Person {
name: string;
age: number;
}
function askSomeone(someone: Developer | Person) {
someone.name;
someone.skill; // error
}
- 유니온 타입을 사용하면 Developer와 Person의 모든 속성을 사용할 수 있을 것이라고 생각할 수 있지만 실제로는 그렇지 않다.
- 타입스크립트 관점에서는 someone이 어떤 값이 들어올 지 모르기 때문에 타입 검증도 없이 공통되지 않은 속성을 사용하게 되면 에러를 발생할 수 있다고 생각한다.
- 그렇기에 이러한 속성을 사용하려면 타입 가드를 사용해야 하고, 일반적으로는 공통된 속성만 사용 가능하다.
Intersection Type
- 인터섹션 타입은 여러 타입을 모두 만족하는 하나의 타입이다.
- 이는
&연산자를 이용해 여러 개의 타입 정의를 하나로 합치는 방식이다.
function askSomeone(someone: Developer & Person) {
someone.name;
someone.skill;
someone.age;
}
- 인터섹션 타입은 Developer과 Person의 모든 속성에 접근이 가능하다.
- 단, 인자로 someone을 넘길 때 Developer과 Person에 해당하는 모든 속성을 넘겨야만 한다.
이넘(Enum)
Enum은 특정 값들의 집합을 의미하는 자료형이다.
숫자형 이넘
타입스크립트에서 숫자형 이넘은 다음과 같이 선언한다.
enum Direction {
Up = 1,
Down,
Left,
Right,
}
- 위와 같이 숫자형 이넘을 선언할 때 초기 값을 주면 초기 값부터 차례로 1씩 증가한다.
- 초기 값을 주지 않으면 0부터 차례로 1씩 증가한다.
- 리버스 매핑은 숫자형 이넘에만 존재하는 특징으로 이넘의 키로 값을, 값으로 키를 얻을 수 있는 특징이다.
문자형 이넘
- 문자형 이넘은 이넘 값 전부 다 특정 문자 또는 다른 이넘 값으로 초기화 해줘야 한다.
- 숫자형 이넘과는 다르게 auto-incrementing이 없다.
enum Shoes {
Nike = "나이키",
Adidas = "아디다스",
}
let myShoes = Shoes.Adidas;
console.log(myShoes); // 아디다스
- 이넘에 문자와 숫자를 혼합하여 생성할 순 있지만 권고하지 않는다.
활용
enum Answer {
Yes = "Y",
No = "N",
}
function askQuestion(answer: Answer) {
if (answer === Answer.Yes) {
console.log("정답입니다");
}
if (answer === Answer.No) {
console.log("오답입니다");
}
}
askQuestion(Answer.Yes);
askQuestion("Yes"); // 에러
- 기존에 문자열로 하드 코딩하던 코드를 enum 타입으로 비교하는 코드로 변경해 안정성과 가독성을 높일 수 있다.
- 파라미터 타입을 enum 타입으로 지정하여 문자열은 인자로 넘길 수 없다.
클래스
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
- 타입스크립트에서의 클래스는 자바스크립트에서의 클래스와는 다르게 초기화할 속성들을 상단에 타입과 함께 정의해둬야 한다.
- constructor 파라미터에서 타입을 정의할 수도 있다.
readonly
클래스의 속성에 readonly 키워드를 사용하면 접근만 가능하다.
class Person {
readonly name: string;
constructor(readonly name: string) {}
}
- readonly를 사용하면 constructor 함수에 초기 값 설정 로직을 넣어줘야 하므로 인자에
readonly키워드를 추가해서 코드를 줄일 수 있다.
Accessor
- 타입스크립트는 객체의 특정 속성의 접근과 할당에 대해 제어할 수 있다.
- 이를 위해서는 해당 객체가 클래스로 생성한 객체여야 한다.
class Developer {
private name: string;
get name(): string {
return this.name;
}
set name(newValue: string) {
this.name = newValue;
}
}
- 위와 같이 name 속성에 제약 사항을 추가하고 싶다면 get과 set을 활용할 수 있다.
제네릭
- 제네릭은 타입을 마치 함수의 파라미터처럼 사용하는 것을 의미한다.
- 한가지 타입보다 여러 가지 타입에서 동작하는 컴포넌트를 생성하는데 사용된다.
function getText<T>(text: T): T {
return text;
}
getText<string>("hi"); // 제네릭 타입이 `<string>`이 됨
getText<number>(10); // 제네릭 타입이 `<number>`이 됨
getText<boolean>(true); // 제네릭 타입이 `<boolean>`이 됨
- 제네릭을 사용하면 함수를 호출할 때 함수에서 사용할 타입 값을 넘길 수 있다.
- 예를 들어
getText<string>('hi')를 호출하면 제네릭은 다음과 같이 동작한다.
function getText<string>(text: string): string {
return text;
}
제네릭을 사용하는 이유
function logText(text: string): string {
return text;
}
위 코드는 인자를 하나 넘겨 받아 반환해주는 함수다. 여기서 함수의 인자와 반환값은 string으로 지정되어 있지만 여러 가지 타입을 허용하고 싶다면 any를 사용할 수 있다.
하지만 any 타입의 경우 함수의 인자로 어떤 타입이 들어갔고 어떤 값이 반환되는지 알 수 없다. any 타입은 타입 검사를 하지 않기 때문이다. 또한 타입은 보다 구체적으로 명시하는 것이 더 좋다.
이러한 문제를 해결하는 것이 제네릭이다.
function logText<T>(text: T): T {
return text;
}
- 함수의 이름 바로 뒤에
<T>라는 코드를 추가한다. 그리고 함수의 인자와 반환 값에 모두 T라는 타입을 추가하면 함수를 호출할 때 넘긴 타입에 대해 타입스크립트가 추정할 수 있게 된다.
이렇게 선언한 함수는 2가지 방법으로 호출 가능하다.
const text = logText<string>("Hello Generic");
const text = logText("Hello Generic");
- 보통 두 번째 방법이 코드도 더 짧고 가독성이 좋기 때문에 흔하게 사용된다.
- 그렇지만 복잡한 코드에서 두 번째 코드로 타입 추정이 되지 않는다면 첫 번째 방법을 사용하면 된다.
인터페이스에 제네릭 선언
interface Dropdown<T> {
value: T;
selected: boolean;
}
const obj: Dropdown<string> = { value: "abc", selected: false };
- 인터페이스를 선언할 때, 제네릭을 사용하여 타입 코드의 양을 줄일 수 있다.
- 예를 들어, value의 값으로 number나 string이 올 수 있는 인터페이스를 작성해야 한다면 각각 따로 작성하는 것이 아닌 제네릭을 활용하여 하나의 인터페이스만 작성할 수 있다.
타입 제한
제네릭 타입 변수
function logText<T>(text: T): T {
console.log(text.length); // Error: T doesn't have .length
return text;
}
- 위와 같이 코드를 작성하게 되면 text에 length 프로퍼티에 접근할 수 없기 때문에 에러가 발생한다.
- 이런 경우에는 제네릭에 타입을 줄 수 있다.
function logText<T>(text: T[]): T[] {
console.log(text.length); // 제네릭 타입이 배열이기 때문에 length를 허용한다.
return text;
}
- 이 코드는 일단 T라는 타입 변수를 받고, 인자 값으로는 배열 형태의 T를 받는다.
T[]대신Array<T>와 같이 좀 더 명시적으로 제네릭 타입을 선언할 수도 있다.
정의된 타입으로 타입 제한
interface LengthType {
length: number;
}
function logTextLength<T extends LengthType>(text: T): T {
console.log(text.length);
return text;
}
logTextLength(10); // 숫자 타입에는 'length'가 존재하지 않으므로 오류 발생
logTextLength({ length: 0, value: "hi" }); // text.length에 접근 가능하므로 오류 없음
- 제네릭 타입 변수 말고도 타입 힌트를 줄 수 있는 방법이 있다.
- 타입 T를 구체적으로 정의하지 않고도 length 속성을 허용하려면 위와 같이 extends 키워드로 특정 인터페이스를 지정한다.
- 위와 같이 작성하게 되면 length에 대해 동작하는 인자만 넘겨받을 수 있게 된다.
keyof로 제네릭 타입 제한
interface ShoppingItem {
name: string;
price: number;
stock: number;
}
function getShoppingItemOption<T extends keyof ShoppingItem>(itemOption: T): T {
return itemOption;
}
getShoppingItemOption(10); // 에러 발생. 인자로는 ShoppingItem의 키값 중 하나만 넘길 수 있다.
- getShoppingItemOption 함수에서 인터페이스의 key 값 중 하나만 받을 수 있도록 제약할 수 있다.
- 제네릭에다가 extends keyof 키워드로 인터페이스를 지정하면 인터페이스에 존재하는 키 중 한 가지가 제네릭이 된다는 뜻이다.
reference
타입스크립트 공식 문서
타입스크립트 핸드북
타입스크립트 입문 - 기초부터 실전까지
0128
전화번호 애플리케이션 실습
지금까지 공부한 타입스크립트 내용을 가지고 간단한 실습을 진행해보았다.
전화번호 리스트를 반환하는 api를 가정하고 코드를 작성한 뒤, 이와 관련된 코드들을 클래스로 묶어서 타입스크립트를 적용해 보았다.
tsconfig 설정
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"target": "es5",
"lib": ["es2015", "dom", "dom.iterable"],
"noImplicitAny": true,
"strict": true,
"strictFunctionTypes": true
},
"include": ["./src/**/*"]
}
- strict와 strictFuncitonTypes를 true로 설정하여 명시적으로 엄격하게 검사하도록 했다.
알게 된 점
Promise를 이용한 API 함수 타입 정의
interface PhoneNumberDictionary {
[phone: string]: {
num: number;
};
}
interface Contact {
name: string;
address: string;
phones: PhoneNumberDictionary;
}
// api
function fetchContacts(): Promise<Contact[]> {
const contacts: Contact[] = [
{
name: "Tony",
address: "Malibu",
phones: {
home: {
num: 11122223333,
},
office: {
num: 44455556666,
},
},
},
{
name: "Banner",
address: "New York",
phones: {
home: {
num: 77788889999,
},
},
},
{
name: "마동석",
address: "서울시 강남구",
phones: {
home: {
num: 213423452,
},
studio: {
num: 314882045,
},
},
},
];
return new Promise((resolve) => {
setTimeout(() => resolve(contacts), 2000);
});
}
- 위와 같이 프로미스를 반환하는 코드이기에 처음에는 함수의 반환값으로 Promise를 지정했다.
- 하지만 비동기 코드 내부에서는 무슨 일이 일어나는지 타입스크립트가 알 수 없기 때문에 프로미스 내부적으로 제네릭을 전달받아야 하도록 구현되어 있다.
- 그렇기에 프로미스를 사용할 때는 제네릭으로 Contact 배열을 전달해줘서 문제를 해결했다.
Enum 활용
PhoneType의 종류가 home, office, studio 세 종류가 존재할 때, 이 중 하나의 값만 받을 수 있도록 enum을 정의하고 활용해보았다.
enum PhoneType {
Home = 'home',
Office = 'office',
Studio = 'studio',
}
findContactByPhone(phoneNumber: number, phoneType: PhoneType): Contact[] {
return this.contacts.filter(
contact => contact.phones[phoneType].num === phoneNumber
);
}
- 위와 같이 작성하니 enum 내부에 값 중 하나인지 체크해주고, 리터럴이 아닌 이넘을 자동완성과 함께 사용하며 개발의 용이성을 느낄 수 있었다.
타입 모듈화
타입스크립트에서도 인터페이스나 enum으로 선언한 것들을 별도의 모듈로 구분하고, import 해서 사용하여 폴더를 구조화 하였다.
// types.ts
interface PhoneNumberDictionary {
[phone: string]: {
num: number;
};
}
interface Contact {
name: string;
address: string;
phones: PhoneNumberDictionary;
}
enum PhoneType {
Home = "home",
Office = "office",
Studio = "studio",
}
export { Contact, PhoneType };
사용법은 ES6의 모듈과 개념이 유사하다.
인터페이스와 enum 등을 어떻게 더 상세하게 폴더 구조화할 지는 추후 프로젝트에 적용하며 더 고민해봐야겠다.
reference
타입스크립트 공식 문서
타입스크립트 핸드북
타입스크립트 입문 - 기초부터 실전까지
0129
최신 브라우저 내부
CPU, GPU, 메모리 그리고 다중 프로세스 아키텍처
CPU
CPU는 컴퓨터의 두뇌라 할 수 있다. CPU 코어는 여러 종류의 작업을 하나씩 순서대로 처리할 수 있다.
GPU
- CPU와 달리 GPU는 간단한 작업에만 특화되어 있지만 여러 GPU 코어가 동시에 작업을 수행할 수 있다.
- GPU는 그래픽 작업을 처리하기 위해 개발되었고, 빠른 렌더링과 매끄러운 상호작용에 도움을 준다.
- 최근 몇 년 동안 GPU 가속을 통해 GPU가 단독으로 처리할 수 있는 계산이 점점 더 많아졌다.
특정 작업을 CPU가 아닌 다른 특별한 장치를 통해 수행 속도를 높이는 것을 하드웨어 가속이라 한다.
그래픽이나 사운드와 관련된 작업에 하드웨어 가속을 많이 사용한다. 브라우저에서 하드웨어 가속은 주로 GPU를 사용한 그래픽 작업의 가속을 의미한다.
프로세스와 스레드로 프로그램 실행
- 브라우저 아키텍처를 살펴보기 전에 파악해야 할 또 다른 개념은 프로세스와 스레드이다.
- 애플리케이션을 시작하면 프로세스가 하나 만들어진다. 프로세스가 작업을 하기 위해 스레드를 생성할 수도 있지만 선택 사항이다.
- 운영체제는 프로세스가 작업할 메모리를 한 조각 주는데, 이 전용 메모리 공간에 애플리케이션의 모든 상태가 저장된다.
- 애플리케이션을 닫으면 프로세스가 사라지고 운영체제가 메모리를 비운다.
- 프로세스는 여러 작업을 수행하기 위해 운영체제에 다른 프로세스를 실행하라고 요청할 수 있다. 그러면 메모리의 다른 부분이 새 프로그램에 할당된다.
- 두 프로세스가 서로 정보를 공유해야 할 때는 IPC를 사용한다.
- 그래서 작업 프로세스가 응답하지 않을 때 애플리케이션의 다른 부분을 실행하는 프로세스를 중지하지 않고도 응답하지 않는 프로세스를 다시 시작할 수 있다.
브라우저 아키텍처
- 브라우저를 만드는 방법에 대한 표준은 없기 때문에 이를 멀티 프로세스로 구현할 지, 멀티 스레드로 구현할 지는 선택사항이다.
- 다만 크롬 브라우저의 경우 브라우저 프로세스가 애플리케이션의 각 부분을 맡고 있는 다른 프로세스를 조정하는 방식으로 구현되어 있다.
- 렌더러 프로세스는 여러 개가 만들어져 각 탭마다 할당된다.
- 최근까지 크롬은 탭마다 프로세스를 할당했다. 이제는 사이트마다 프로세스를 할당한다.
브라우저 프로세스
주소 표시줄, 북마크 막대, 뒤로 가기 버튼, 앞으로 가기 버튼 등 애플리케이션의 크롬 부분을 제어한다.
네트워크 요청이나 파일 접근과 같이 눈에 보이지는 않지만 권한이 필요한 부분도 처리한다.
렌더러 프로세스
탭 안에서 웹 사이트가 표시되는 부분의 모든 것을 제어한다.
플러그인 프로세스
웹 사이트에서 사용하는 플러그인을 제어한다.
GPU 프로세스
GPU 작업을 다른 프로세스와 격리해서 처리한다. GPU는 여러 애플리케이션의 요청을 처리하고 같은 화면에 요청받은 내용을 그리기 때문에 GPU 프로세스는 별도 프로세스로 분리되어 있다.
다중 프로세스 아키텍처라 크롬에 주는 이점
- 예를 들어, 렌더러 프로세스를 하나 사용하는 경우 여러 탭 중 하나의 탭만 응답하지 않아도 모든 탭이 응답하지 못하게 된다.
- 반면, 각 탭이 독립적인 렌더러 프로세스에 의해 실행되는 경우 한 탭이 응답히자 않으면 그 탭만 닫고 실행 중인 다른 탭으로 이동할 수 있다.
- 같은 원리로 브라우저 프로세스와 렌더러 프로세스를 분리하여 렌더러 프로세스가 웹 페이지를 그리는 도중 오류가 발생해 실행이 중단되더라도 브라우저 전체가 종료되지 않고 오류 페이지를 보여 주는 정도로 문제를 해결할 수 있다.
보안과 격리
- 운영체제를 통해 프로세스의 권한을 제한할 수 있어 브라우저는 특정 프로세스가 특정 기능을 사용할 수 없게 제한할 수 있다.
- 예를 들어 크롬은 렌더러 프로세스처럼 임의의 사용자 입력을 처리하는 프로세스가 임의의 파일에 접근하지 못하게 제한한다.
더 많은 메모리 절약 - 크롬의 서비스화
크롬은 브라우저의 각 부분을 서비스로 실행해 여러 프로세스로 쉽게 분리하거나 하나의 프로세스로 통합할 수 있도록 아키텍처를 변경하고 있다.
- 성능이 좋은 하드웨어에서 크롬이 실행 중일 때에는 각 서비스를 여러 프로세스로 분할해 안정성을 높이고, 리소스가 제한적인 장치에서 실행 중일 때에는 서비스를 하나의 프로세스에서 실행하여 메모리 사용량을 줄이는 것이 기본 아이디어이다.
프레임별로 실행되는 렌더러 프로세스 - 사이트 격리
- 사이트 격리는 iframe의 사이트를 별도의 렌더러 프로세스에서 실행하는 것이다.
- 탭마다 렌더러 프로세스를 할당하는 모델에서는 iframe의 사이트가 같은 렌더러 프로세스에서 작동하기 때문에 서로 다른 사이트 간에 메모리가 공유될 수 있다는 문제점이 있었다.
- 동일 출저 정책(sop)은 웹 보안 모델의 핵심으로 한 사이트는 동의 없이 다른 사이트의 데이터에 접근할 수 없어야 한다. 이때 프로세스를 격리하는 것이 사이트를 격리하는 가장 효과적인 방법이다.
내비게이션 과정
내비게이션이란 브라우저 주소 표시줄에 URL을 입력하면 브라우저가 인터넷에서 데이터를 가져와서 페이지 렌더링을 준비하는 과정을 의미한다.
- 브라우저 프로세스는 탭 영역 밖에 있는 모든 부분을 제어한다. 브라우저 프로세스에는 UI 스레드와 네트워크 스레드, 스토리지 스레드 등이 있다
- UI 스레드는 브라우저의 버튼과 입력란을 그린다.
- 네트워크 스레드는 인터넷에서 데이터를 가져오기 위해 네트워크 스택을 다룬다.
- 스토리지 스레드는 파일에 대한 접근을 제어한다.
- 주소 표시줄에 URL을 입력하면 브라우저 프로세스의 UI 스레드가 입력을 처리한다.
1단계: 입력 처리
- 사용자가 주소 표시줄에 타이핑을 시작하면 UI 스레드는 먼저 ‘입력되는 내용이 검색어인지 URL인지’ 확인한다.
- UI 스레드는 입력되는 내용을 파싱하여 검색 엔진으로 이동할지 요청한 사이트로 이동할지 결정해야 한다.
- 사용자가 입력한 문자열이 검색어라면 문자열을 사용자가 선택한 검색 엔진의 URL과 조합해 새로운 URL 형태로 변환한다.
2단계: 내비게이션 시작
- 사용자가 Enter 키를 누르면 사이트의 콘텐츠를 가져오기 위해 UI 스레드가 네트워크 호출을 시작한다.
- 로딩 스피너가 탭의 모서리에 표시되고, 네트워크 스레드는 요청을 처리한다.
- 네트워크 스레드가 301과 같은 리디렉션 헤더를 수신할 경우 네트워크 스레드가 UI 스레드와 통신해 서버가 리디렉션을 요청했다는 것을 알린다. 그런 다음 새로운 URL 요청이 시작된다.
3단계: 응답 읽기
- 응답 본문인 페이로드가 들어오기 시작하면 네트워크 스레드는 스트림의 처음 몇 바이트를 확인한다.
- 페이로드가 어떤 형식의 데이터인지는 응답 헤더의 Content-Type 헤더가 알려 주지만 정보가 없거나 잘못된 정보가 있을 수 있다.
- 이때 MIME 스니핑을 실행해 데이터의 실제 형식을 알아낸다.
- 응답이 HTML 파일이라면 데이터를 렌더러 프로세스에 전달하는 단계로 넘어간다.
- 이 단계는 또한 Safe Browsing의 검사가 실행되는 단계이다. 도메인과 응답 데이터가 악성사이트로 알려진 사이트와 일치하는 것 같다면 네트워크 스레드는 경고 페이지를 표시하라고 알린다.
3단계: 렌더러 프로세스 찾기
- 모든 검사가 끝나고 브라우저가 요청된 사이트로 이동해야 한다고 네트워크 스레드가 확신하게 되면 네트워크 스레드는 UI 스레드에 데이터가 준비되었음을 알린다.
- UI 스레드는 웹 페이지의 렌더링을 수행할 렌더러 프로세스를 찾는다.
- 네트워크 요청이 응답을 받기까지 수백 밀리초가 걸릴 수 있기 때문에 이 과정을 더 빨리 진행하기 위한 최적화가 적용되어 있다.
- 2단계에서 UI 스레드가 네트워크 스레드로 URL 요청을 보낼 때 UI 스레드는 이미 어느 사이트로 이동할 지 알고 있기에 네트워크 요청과 동시에 렌더러 프로세스를 시작한다.
- 모든 것이 예상대로 잘 진행된다면 네트워크 스레드가 데이터를 받을 때 렌더러 프로세스는 준비 상태에 있게 된다.
4단계: 내비게이션 실행
- 데이터와 렌더러 프로세스가 준비되었으므로 내비게이션을 실행하도록 브라우저 프로세스에서 렌더러 프로세스로 IPC 메시지를 전송한다.
- 그리고 렌더러 프로세스가 HTML 데이터를 계속 수신할 수 있도록 브라우저 프로세스는 데이터 스트림을 전달한다.
- 렌더러 프로세스에서 내비게이션이 실행되었다는 것을 브라우저 프로세스가 확인하고 나면 내비게이션이 완료되고 문서 로딩 단계가 시작된다.
- 이 시점에 주소 표시줄이 업데이트 되고 보안 표시와 사이트 설정 UI도 새 페이지의 사이트 정보를 반영해 갱신된다.
- 탭에 대한 세션 기록이 업데이트되어 뒤로 가기 버튼과 앞으로 가기 버튼도 방금 이동한 사이트를 반영해 작동한다.
- 탭이나 창을 닫은 이후 탭과 세션을 복원할 수 있게 세션 기록이 디스크 드라이브에 저장된다.
추가 단계: 초기 로드 완료
- 내비게이션이 실행되면 렌더러 프로세스는 계속 리소스를 로딩하고 페이지를 렌더링한다.
- 렌더러 프로세스가 렌더링을 끝내면 브라우저 프로세스로 IPC 메시지를 보낸다.
- 그러면 UI 스레드는 탭에서 로딩 스피너의 작동을 중지한다.
다른 사이트로 내비게이션
- 내비게이션이 완료된 후, 다른 URL을 다시 입력하게 되면 브라우저 프로세스는 동일한 단계를 거쳐 다른 사이트로 이동을 처리한다.
- 하지만 그전에 렌더링된 사이트에서 beforeunload 이벤트를 확인해야 한다.
- 이 이벤트는 내비게이션 시간이 늘어날 수 있기에 페이지에 입력한 데이터가 손실될 수 있음을 경고하는 등 필요한 경우에만 추가해야 한다.
서비스 워커
최근 서비스 워커가 도입되며 내비게이션 과정에도 변화가 생겼다.
- 서비스 워커는 애플리케이션의 코드에 네트워크 프록시를 작성할 수 있는 수단이다.
- 서비스 워커를 통해 웹 개발자는 무엇을 로컬 캐시에 저장할지, 언제 네트워크에서 새 데이터를 가져올지 제어할 수 있다.
- 서비스 워커가 캐시에서 페이지를 로드하도록 설정되었다면 네트워크에서 데이터를 가져오도록 요청할 필요가 없다.
- 중요한 점은 서비스 워커가 렌더러 프로세스에서 실행되는 JS 코드라는 점이다. 그렇다면 내비게이션 요청이 들어왔을 때 브라우저 프로세스는 사이트에 서비스 워커가 있다는 것을 어떻게 알 수 있을까?
- 서비스 워커가 등록되면 서비스 워커의 범위는 참조로 유지된다.
- 내비게이션이 발생하면 네트워크 스레드는 도메인을 등록된 서비스 워커의 범위와 비교한다.
- 해당 URL에 등록된 서비스 워커가 있으면 UI 스레드는 서비스 워커 코드를 실행하기 위해 렌더러 프로세스를 찾는다.
- 서비스 워커는 네트워크에 데이터를 요청하지 않고 캐시에서 데이터를 가져올 수 있다.
내비게이션 프리로드
- 브라우저 프로세스와 렌더러 프로세스 사이를 왕복해야 하는 상황에서 서비스 워커가 결국 네트워크에서 데이터를 요청하기로 하면 지연이 발생하게 됨을 알 수 있다.
- 내비게이션 프리로드는 서비스 워커의 시작과 병렬로 리소스를 로딩해 내비게이션 과정의 속도를 높이는 메커니즘이다.
- 이 요청은 헤더에 표시되어 서버가 이러한 요청에 대해 다른 콘텐츠를 보낼 수 있게 한다.