CH1. 리액트 개발을 위해 꼭 알아야할 자바스크립트
1.1 자바스크립트의 동등 비교
1.1.1 JS의 데이터 타입
-
원시 타입
- boolean
- null
- 명시적으로 비어 있는 값
- type of 의 결과는 object 가 나온다
- undefined
- 선언 되었으나 할당되지 않은 값
- number
- 최대 2^53 - 1 까지 표현 가능
- bigint
- 2^53 - 1 이상의 수 표현 가능
- string
- symbol
- 중복되지 않는 고유한 값, 함수로만 생성 가능
** 세미 콜론 안쓰는거 저만 불편한가요? ㅋㅋㅋㅋㅋㅋ
const key1 = Symbol("key");
const key2 = Symbol("key");
key1 === key2; //false
// 동일한 값을 위해서넌 Symbol.for 사용
** [p.26] Symbor 은 언제 쓰일까? 그리고 Symbor.for 로 동일한 값을 사용하면 그냥 일반 값을 쓰는 것과 무슨 차이일까?
-
객체 타입
- object
- 참조를 전달하기 때문에 참조 타입(reference type)으로 불리운다
1.1.2 값을 저장하는 방식의 차이
-
원시 타입과 객체 타입의 차이
- 원시 타입은 메모리에 값 자체를 저장
- 객체 타입은 프로퍼티를 수정할 수 있으므로 원시 타입과 다르게 변경 가능한 형태로 저장, 값을 복사 할 때도 참조 값이 복사
- 단, 객체 내부의 프로퍼티의 값은 원시 형태로 저장
1.1.3 자바스크립트의 또다른 비교 공식 Object.is
- ES6(2015)에 도입 된 비교 문법, === 의 한계를 극복하기 위해 도입
- === 와 동일하게 자동 형변환을 하지 않고 비교
- === 가 하지 못하는 비교 가능
- 객체간의 비교는 === 와 동일하게 비교
-0 === +0; // true
Object.is(-0, +0); //false
// (x !== 0 || 1 / x === 1 / y) 를 사용해서 구분
Number.NaN === NaN; // false
Object.is(Number.NaN, NaN); // true
// (x !== x && y !== y) 를 이용해 구분
NaN === 0 / 0; // false
Object.is(NaN, 0 / 0); // true
// (x !== x && y !== y) 를 이용해 구분
** [p.29] 이게 리얼루다가 제정신인 언어냐......
1.1.4 리액트에서의 동등 비교
-
리액트에서 사용하는 동등 비교는 Object.is 를 사용
-
ES6 하위 호환을 위해 별도로 구현한 polyfill(이전 브라우저에서 최신 기능 사용을 위한 코드) 사용
-
리액트에서 아용하는 비교 코드
-
TS 버전
// polyfill
function is(x: any, y: any) {
return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
}
// 일반
const objectIs: (x: any, y: any) => boolean =
typeof Object.is === "function" ? Object.is : is;
- JS 버전
// polyfill
function is(x, y) {
return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
}
// 일반
const objectIs = typeof Object.is === "function" ? Object.is : is;
const obj1 = {};
const obj2 = {};
const objCopy = obj1;
console.log(is(obj1, obj2)); // false
console.log(is(obj1, objCopy)); // true
console.log(objectIs(obj1, obj2)); // false
console.log(objectIs(obj1, objCopy)); // true
** [p. 29] -0 과 +0의 비교와 NaN이 각기 다르다는 점을 명확히 이해하게 되어서, 추후에 이게 왜이러지? 하는 상황은 거의 없긴 하겠지만 피할 수 있을듯
** 그런데 이러니 JS가 노근본이라 욕을 먹지......
// 전체 수식
if (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
// 첫번째 수식
if (x === y)
// 두번째 수식
if (x !== 0 || 1 / x === 1 / y)
// 세번째 수식
if (x !== x && y !== y)
- 먼저 첫번째 비교식으로 기본적인 JS의 비교 성립
- 두번째 비교식(x !== 0 || 1 / x === 1 / y)을 통해 -0 과 +0 을 비교 가능
- -0, +0 은 0 과 같다고 판단 되므로 x !== 0 에서 false 해당 비교식은 OR(||) 로 연결되어 있으니 다음 비교식 진행
- 1 / x 와 1 / y 는 각각 부호가 붙은 인피니티가 나오기 때문에 서로 다르다! 따라서 해당 비교식에서 false
- 그럼 두번째 비교식이 둘 다 false 처리가 되므로 해당 비교식은 false -> 전체 비교문은 AND(&&) 로 처리 되어 있으므로 false 결과 반환
- 세번째 비교식(x !== x && y !== y)을 통해 NaN 이 동등하다는 비교가 가능
- NaN은 특이한 성질을 같는데, NaN은 JS의 전역 Number 객체의 프로퍼티다. 그리고 서로 동일 비교를 하면 false 가 뜬다
- 따라서 NaN 끼리는 동등 비교를 통해 true 를 도출 할 수 없다
- 이를 해결하기 위해 NaN이 세번째 비교식에 들어가면 둘다 true 가 뜨므로 다양한 NaN 이 동등하다고 비교가 가능하다
- 추가 isNaN() 과 Number.isNaN() 의 차이
- isNaN은 인자를 숫자로 일단 한번 변환을 시킨 후, 해당 값이 NaN이면 true -> 문자열이나 숫자 이외의 값을 true 처리
- Number.isNaN은 변환 과정 없이 인자가 정확히 NaN 인 경우만 true 처리
1.1.4 리액트에서의 동등 비교
- 리액트는 일단 Object.is 로 동등 비교를 한다
- 동등 비교가 안되는 객체의 경우는 depth 1 까지만 동등 비교(얕은 비교)를 한다
- 이로 인해서 객체가 depth 2 이상을 가지게 되면, 값이 변화하지 않아도 무조건 렌더링이 되는 문제 발생
- useState 에서 객체를 잘 못 전달하면 무한 렌더링이 발생하는 이슈도 이러한 비교 특성으로 인한 문제
- 재귀를 활용해서 객체를 깊은 비교를 했다면?
- 성능 이슈가 발생할 가능성이 높아서 제외
- 잘못된 객체 하나로 인하여 페이지 자체가 멈춰버리는 현상 발생 -> 프론트 입장에서는 최악의 결과
import { memo, useEffect, useState } from "react";
type Props = {
counter: number,
};
const Component = memo((props: Props) => {
useEffect(() => {
console.log("Component 랜더!");
});
return <h1>{props.counter}</h1>;
});
// Depth 가 2 이상이므로 memo 가 역할을 하지 못하고 무조건 렌더링이 된다.
type DeeperProps = {
counter: {
counter: number,
},
};
const DeeperComponent = memo((props: DeeperProps) => {
useEffect(() => {
console.log("DeeperComponent 랜더!");
});
return <h1>{props.counter?.counter}</h1>;
});
export default function Chapter01() {
const [, setCounter] = useState(0);
const handleClick = () => {
setCounter((prev) => prev + 1);
};
return (
<div>
<Component counter={100} />
<DeeperComponent counter={{ counter: 100 }} />
<button onClick={handleClick}>리렌더</button>
</div>
);
}
1.1.5 정리
- JS의 이러한 언어적 특성(=한계) 이해해야만 정확한 리액트 구현이 가능
- 추후 의존성 배열, useMemo, useCallback, React.memo 에도 동일하게 적용 되므로 이를 바탕으로 최적화 가능
1.2 함수
1.2.1 함수란 무엇인가?
- 작업을 수행하거나 값을 계산하는 등의 과정을 표현하고, 이를 하나의 블록으로 감사서 실행 단위로 만들어 놓은 것
1.2.2 함수를 정의하는 4가지 방법
함수 선언문
- 가장 일반적인 방식
- 선언문은 표현식이 아니므로 변수에 할당이 불가능해 보이지만, JS 엔진이 자동으로 표현식으로 해석하여 할당 가능
- 호이스팅 지원
const sum = function sum(a, b) {
return a + b;
};
함수 표현식
- JS에서 함수는 일급 객체(= 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체)이다
- 따라서 함수는 다른 함수의 매개변수, 반환 값으로 사용이 가능
- 혼란을 방지하기 위해 함수의 이름은 생략하여 사용
- 호이스팅 미지원
const sum = function (a, b) {
return a + b;
};
** [p. 39] 둘의 사용은 취향의 차이, 회사의 협업자의 컨벤션을 따르는게 좋다고 생각
Function 생성자
- 실제로는 거의 사용되지 않는 방식
- 실제로 매겨변수를 전부 문자열로 선언해야함
- 클로저가 생성되지 않음
const sum = new Function("a", "b", "return a+b");
화살표 함수
- ES6 에서 새롭게 추가
- 타이핑하는 글자수가 줄어드는 효과
- 생성자(this) 사용 불가능 / argumenrs 가 제공되지 않음
const sum = (a, b) => a + b;
- 클래스 컴포넌트에서 this 를 사용할 때, 함수 선언문으로 컴포넌트를 만들어서 this.setState 를 사용하려고 하면 this 가 컴포넌트 내부의 this 를 가르키기 때문에 this. setState 사용이 불가능
- 화살표 함수를 사용하면, 만들어 지는 시점에서 자동으로 상위 스코프의 this 를 받아오므로, 클래스 컴포넌트의 this 를 받아서 this.setState 사용 가능
import React, { Component } from "react";
export default class Chapter01_1 extends Component {
constructor(props) {
super(props);
this.state = {
counter: 1,
};
}
functionCountUp() {
// functionCountUp 함수의 this 를 가르킴
console.log(this);
this.setState((prev) => ({ counter: prev.counter + 1 }));
}
arrowFunctionCountUp = () => {
// 삼위 클래스 컴포넌트의 this 를 가르킴
console.log(this);
this.setState((prev) => ({ counter: prev.counter + 1 }));
};
render() {
return (
<div>
<button onClick={this.functionCountUp}>일반 함수</button>
<button onClick={this.arrowFunctionCountUp}>화살표 함수</button>
</div>
);
}
}
1.2.3 다양한 함수 살펴보기
즉시 실행 함수
- IIFE(Immediately Invoked Function Expression)
- 정의되는 즉시 실행되는 함수
- 다시 호출이 불가능하여, 보통 이름을 붙이지 않는다
(function (a, b) {
return a + b;
})(10, 24);
((a, b) => a + b)(10, 24);
고차 함수
- 함수를 인수로 받거나, 새로운 함수를 반환하는 함수
- 이러한 특징을 활용하여 다른 컴포넌트를 인수로 받아 새로운 함수 컴포넌트를 반환하는 고차 함수 작성 가능
- 컴포넌트 내부에서 공통으로 관리되는 로직을 분리하여, 효율적 리팩토링이 가능
1.2.4 함수를 만들 때 주의해야 할 사항
- 부수 효과를 최대한 억제할 것
- 액션을 제거하고 계산만 남기는 컴포넌트를 작성할 것(?)
- 가능한 함수를 작게 만들 것
- 누구나 이해할 수 있는 이름을 붙여라
- Teeser 를 사용하면 한글로 네이밍 가능
1.3 클래스
1.3.1 클래스란 무엇인가?
- JS의 객체를 만들기 위한 일종의 템플릿 개념
- constructor (생성자)
- 클래스 생성시 객체의 값을 정하는 특수 메서드, 하나만 사용가능
- property (프로퍼티)
- 생성자에서 정의할 수 있는 속성 값
class Car {
constructor(name) {
this.name = name;
}
}
const hyundaiCar = Car("hyundai");
- getter / setter
- 프로퍼티의 값을 지정하거나 받는 메서드, get / set 예약어 사용
class Car {
constructor(name) {
this.name = name;
}
get getName() {
return this.name;
}
set setName(name) {
this.name = name;
}
}
- 인스턴스 메서드
- 클래스 내부에 직접 정의한 메서드
class Car {
constructor(name) {
this.name = name;
}
hello() {
console.log(`안녕하세요. ${this.name} 입니다!`);
}
}
- 정적 메서드
- 인스턴스 없이 이름으로 호출 가능한 메서드
- 전역에서 사용하는 유틸 함수를 위해 주로 사용
class Car {
constructor(name) {
this.name = name;
}
static hello() {
console.log("안녕하세요!");
}
sayName() {
console.log(this.name);
}
}
const hyundaiCar = new Car("hyundai");
Car.hello();
hyundaiCar.sayName();
hyundaiCar.hello(); // 에러 발생
- 상속
- 클래스를 기반하여 확장하는 개념
class Car {
constructor(name) {
this.name = name;
}
}
class Truck extends Car {
constructor(name) {
super(name);
}
load() {
console.log("짐 싣기");
}
}
const truck = new Truck();
truck.load();
1.3.2 클래스와 함수의 관계
- ES6에서 나온 개념
- prototype 을 사용하여 구현하던 것을 객체지향 언어를 사용하는 프로그래머들이 좀 더 쉽게 JS 를 사용할 수 있도록 해주는 Syntactic Sugar 로서 적용
1.4 클로저
1.4.1 클로저의 정의
- MDN 정의 : 클로저는 함수와 함수가 선언된 어휘적 환경(Lexical Scope)의 조합
- 내가 생각한 정의 : 클로저는 함수가 정의된 시점에 결정되는 변수의 유효 Scope를 함수가 기억하고 실행 시 적용하는 것
** [p.59] 진짜로 MDN 정의 보고 바로 이해하신분?
1.4.2. 변수의 유효 범위, 스코프
전역 스코프
- 전역 레벨에서 사용되는 스코프
- 브라우저 전역 객체인 window / Nodejs 전역 객체인 global 등
- var 변수는 전역 스코프로 사용 된다
함수 스코프
- JS 는 기본적으로 함수 레벨의 스코프를 따른다
- 블록 범위 스코프가 아닌, 함수 정의 레벨에서의 스코프를 따름
- 해당 변수가 호출 된 시점에서 가장 가까우 함수 레벨의 스코프를 참조
** [p.61] 이건 var 기준! let, const 는 블록 스코프를 따른다! let, const 없을 때 디버깅은 리얼 Hell....
var x = 10;
function foo() {
var x = 100;
console.log(x); // 100
function bar() {
var x = 1000;
console.log(x); // 1000
}
}
console.log(x); // 10
1.4.3 클로저의 활용
- 클로저를 활용하면 전연 스코프의 사용을 막고, 개발자가 원하는 정보만 노출이 가능
** [p.64] 그런데 요즘은 let, const 를 쓰기 때문에 전역으로 let 을 선언해서 사용하는 것이 아닌 이상 크게 걱정할 내용은 아닌듯 합니다.
리액트에서의 클로저
- useState 가 이미 호출이 끝났지만, 클로저의 특성으로 인하여 setState 함수는 useState 스코프의 state 값을 기억하고 있으며, 해당 값을 컨트롤 가능
function Component() {
const [state, setState] = useState(0);
// 클로저의 특성으로 인하여 setState 는 명시적 표현 없이 state 의 값을 기억
const handleClick = () => {
setState((prev) => prev + 1);
};
}
1.4.4 주의할 점
- 클로저의 특성으로 인하여 의도하지 않은 동작 가능
- for 문은 setTimeout 을 기다리지 않고 바로 실행되어 종료 -> 이 때 i 는 전역 스코프를 가지므로 5로 변경된 상태
- setTimeout 는 실행된 상태로 콜스택에 쌓이고, 2번째 인수로 받은 시간이 되면 콜스택의 콜백 함수를 실행
- 이때 결국 참조되는 것은 전역 i 이므로 5만 출력
- 블록 스코프를 가지는 let 또는 IFEE 를 사용하여 함수 스코프를 이용 해결 가능!
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, i);
}
// 블록 스코프 이용
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, i);
}
// IFEE 로 함수 스코프를 한번 더 전달
for (var i = 0; i < 5; i++) {
setTimeout(
(function (sec) {
return function () {
console.log(sec);
};
})(i),
i * 1000
);
}
- 클로저는 해당 Lexical Scope 를 기억해야 하므로 메모리 사용량이 커지는 등의 문제가 생기므로 성능에서 고려할 점이 있다
- 클로저 사용이 꼭 필요한 작업만 남겨서 잘 사용해야 한다 -> 클로저는 공짜가 아니다!
1.5 이벤트 루프와 비동기 통신의 이해
1.5.1 싱글 스레드 자바스크립트
- JS 는 탄생 당시 간단한 작업을 하기 위해 만들어졌으므로, 동시성 문제 혹은 메모리 공유 문제를 피하기위해 싱글 스레드로 만들어짐
- 추후 JS 에 요구되는 기능이 많아짐으로써 V8, Spider Monkey 등의 런타임에 비동기적 처리 방식이 추가
1.5.2 이벤트 루프란?
호출 스택과 이벤트 루프
- 이벤트 루프는 함수의 호출 스택이 비어있는지를 확인하는 역할을 한다
- JS 는 싱글 스레드로 동작하지만, 비동기 함수 혹은 Wep API 호출 등은 엔진에 의해 다른 스레드가 수행
- 다른 스레드가 수행을 마친 호출의 콜백이 콜스택이 완전히 비었을 때 태스크 큐로 들어가는 구조를 가진다
** [p. 75] 참고하기 좋은 블로그
1.5.3 태스크 큐와 마이크로 태스크 큐
- 이벤트 루프는 태스크 큐와 마이크로 태스크 큐를 가진다
- 매크로 태스크 큐는 태스크 큐 또는, 콜백 큐라고 불리운다
- ES6 에 들어 Promise 문법이 추가되어 마이크로 태크스 큐가 추가 되었고 마이크로 태스크 큐는 잡 큐 라고도 불리운다
- 매크로 태스크 큐(= Task Queue, Callback Queue)는 setTimeout, setInterval, addEventListener, Web API 호출 같은 일을 처리한다
- 마이크로 태스크 큐(= Job Task Queue)는 Promise 의 callback, async 함수의 await 부분 등등 태스크를 처리한다
- 마이크로 태스크 큐(= Job Task Queue)는 태스크 큐(= Macro Task Queue) 보다 우선권을 가진다
- 따라서 Promise, async 함수 등은 setTimeout, setInterval 보다 먼저 수행 된다
** [p. 76] 매크로와 마이크로 큐의 일들이 콜 스택에 들어간다는 것 잊지 말기! 콜 스택은 따로 존재한다!
** [p. 76] 서버 통신 Web API 관련은 처리는 전부 매크로 태스크 큐에서 실행한다!
- XMLHttpRequest, fetch 함수등은 Web API 에 의해 매크로 태스크 큐(= Callback Queue)에 등록이 된다
- 여기서 XMLHttpRequest, fetch 에 의해 서버 통신이 발생하고 통신의 응답으로 이행되는 콜백 / Promise 또는 await 는 마이크로 태스크 큐에서 처리
- 즉, 서버 통신 등록 자체는 이벤트 루프에 의해 순서대로 매크로 태스크에 등록이 되나 해당 통신이 이행되었을 때는 응답을 빠르게 처리해줘야 하므로 마이크로 태스크 큐에 등록하여 다음 매크로 태스크 작업을 수행하기 전에 처리를 완료한다.
- 이런 이유로 인하여 마이크로 태스크가 매크로 태스크에 우선하는 특성을 가진다
** [p. 76] JS가 지속적으로 새로운 개념과 기능을 추가하다보니 헷갈리는 부분이 정말 많고, 기존 이름을 새롭게 변경하거나 추가하는 경우까지 겹쳐서 정말 혼동이 쉽게 오는 것 같습니다!
function foo() {
console.log("foo");
}
function bar() {
console.log("bar");
}
function baz() {
console.log("baz");
}
setTimeout(foo, 0);
Promise.resolve().then(bar).then(baz);
// bar -> baz -> foo 순으로 실행
렌더링은 언제 일어나는가?
- 렌더링은 마이크로 태스크 큐 작업이 끝날 때마다 실행된다!
- 즉, 마이크로 태스크와 매크로 태스크 큐 사이에서 발생
** [p. 77] 마이크로 태스크 큐에 들어간 작업 하나가 끝날 때마다 랜더링이 되는지, 아니면 모든 마이크로 태스크 큐가 끝난 후에 랜더링이 되는지 확인
- 예제에 아래의 코드를 추가하여 확인
const TIMES = 100000;
test.addEventListener("click", () => {
for (let i = 0; i <= TIMES; i++) {
queueMicrotask(() => {
console.log("fisrt", i);
});
}
for (let i = 0; i <= TIMES; i++) {
queueMicrotask(() => {
console.log("second", i);
});
}
for (let i = 0; i <= TIMES; i++) {
setTimeout(() => {
test.innerHTML = i;
console.log("MACRO");
}, 100);
}
});
- test 버튼 클릭이 발생하면 들어있는 3가지 for 문이 실행
- for 문에 들어있는 queueMicrotask 와 setTimeout 은 JS의 비동기적 특성에 의해 거의 동시이긴 하지만 순서대로 호출
- 제일 처음의 마이크로 태스크 큐에 들어간 for 문이 콘솔 창에 "first, i" 를 띄우면 실행 -> 이때까진 랜더링 X
- "fisrt, 100000" 이 뜨면 버튼의 text 값이 변경(랜더링)이 되면서 "second, i" 콘솔이 뜨기 시작
- 따라서, 랜더링은 마이크로 태스크 큐에 들어간 작업 하나가 끝나면 랜더링이 되는 것으로 볼 수 있다.
- "second, 100000" 이 콘솔에 찍히면 마이크로 태스크 큐가 완전히 비게 되므로 그때 setTimeout 에 의해 들어간 "MACRO" 콘솔이 10001번 찍힘
- 사실 이게 말이 되는 부분, 하나의 마이크로 큐 태스크로 작업 마친 것(아마도 서버 통신의 응답 같은 것)을 화면에 즉시 랜더링 해야할 일이 발생 할 텐데, 이때 모든 마이크로 큐 태스크를 전부 실행 후에 랜더링이 발생하면 프로그래머가 원하는 상황과는 다른 일이 생길듯
1.6 리액트에서 자주 사용하는 자바스크립트 문법
1.6.1 구조 분해 할당(Destructuring assignment)
- 배열 또는 객체의 값을 분해하여 개별 변수에 할당하는 것
- ES6 에서 첫 도입!
배열 구조 분해 할당
- 구조 분해 할당에서 기본값이 사용되는 것은 undefined 값이 예상될 때 뿐이다.
const [a = 1, b = 1, c = 1, d = 1, e = 1] = [undefined, null, 0, ""];
console.log(a, b, c, d, e);
// a, e 만 기본 값이 설정
- 전개 연산자 사용 가능
객체 구조 분해 할당
- 객체 내부의 이름으로 값을 꺼내오는 할당 법
- 새로운 이름으로 할당 가능 / 기본값 설정 가능
- 리액트 props 에서 주로 사용
- 전개 연산자 사용 가능
- 바벨에 의해 트랜스파일이 되면 번들링 크기가 상대적으로 크므로 사용시 성능 이슈를 고려할 필요가 있다
const obj = {
a: 1,
b: 1,
};
const { a = 10, b = 10, c = 10 } = obj;
console.log(a, b, c); // 1, 1, 10
1.6.2 전개 구문
배열의 전개 구문
- 배열을 쉽게 합성 가능
- 기존 배열에 영향을 미치지 않고 복사 가능 (값만 복사되고 참조는 다름)
const arr1 = [1, 2];
const arr2 = [...arr1, 3, 4, 5];
console.log(arr2); // [1, 2, 3, 4, 5];
객체의 전개 구문
- 객체를 합성하는데 편리하게 사용 가능
- 순서가 중요하므로 순서를 잘 지켜서 사용해야 함
const obj1 = {
a: 1,
b: 2,
};
const obj2 = {
...obj1,
c: 3,
};
consloe.log(obj2); // { a: 1, b: 2, c: 3};
1.6.3 객체 초기자
- 이미 변수로 선언된 값이 있으면 바로 객체 내부에 넣는 것이 가능
- ECMAScript 2015 에 도입된 기능
const a = 1;
const b = 2;
const obj = {
a,
b,
};
console.log(obj); // {a: 1, b: 2}
1.6.4 Array 프로토타입의 메서드(map, filter, reduce, forEach)
map
- 전달 받은 배열에 특정 콜백 함수의 작업을 마친 뒤, 같은 길이의 배열을 반환하는 메서드
filter
- 콜백 함수의 조건 결과에 따라 원소 반환을 결정하는 메서드
reduce
- 콜백 함수를 실행하고, 이를 초깃값에 누적해서 결과를 반환하는 메서드
- 다양한 활용이 가능
// filter 를 reduce 로 구현
const arr = [1, 2, 3, 4, 5];
const result = arr.reduce((acc, cur) => {
if (cur % 2 === 0) {
acc.push(cur);
}
return acc;
}, []);
console.log(result); // [2, 4]
forEach
- forEach 는 반환 값이 없고 콜백 함수만 실행하므로 주의 필요
- 또한 break, rerturn 등으로 순회를 멈출 수 없으므로 주의 필요
1.6.5 삼한 조건 연산자
- JS 에서 유일하게 3개의 피연산자를 취할 수 있는 문법
- 중첩 되면 가독성이 떨어지므로 가급적 중첩을 피하는 것이 좋다
1.7 선택이 아닌 필수, 타입스크립트
- 필수다!
1.7.1 타입스크립트란?
- JS 에 타입을 가미한 것
- JS 의 한계인 동적 타입 언어가 가지는 문제를 해결하기 위해 도입
- 런타임이 아닌 빌드(트랜스파일, 컴파일) 타임에 타입 체크를 수행
- 빌드 타임에 에러가 발생할 가능성이 있는 코드를 체크 가능
- 결국 TS 는 JS 의 슈퍼셋일 뿐(= 결국 JS 로 변환 되므로)이므로, JS 에서 불가능한 작업을 TS 에서 수행하는 것은 불가능
1.7.2 리액트 코드를 효과적으로 작성하기 위한 타입스크립트 활용법
any 대신 unknown 을 사용하자
- any 를 사용하면 TS 를 사용하는 장점을 버리는 것과 마찬가지
- any 대신 unknown 을 사용하기
- nerver 를 사용하여 사용하지 않는 값 처리하기
- unknown 을 사용하기 위해서는 해당 변수의 type narrowing 을 해줘야만 사용이 가능하기 때문에 any 로 부터 발생하는 문제점 해결이 가능!
function doSomthing(cb: unknown) {
if (typeof cb === "function") {
cb();
return;
}
throw new Error("cb 은 함수여야 합니다!");
}
doSomthing(1); // throw Err
타입 가드를 적극 활용하자
instanceof 와 typeof
- instanceof 로 지정한 인터스턴스가 특정 클래스의 인스턴스인지를 확인하여 원하는대로 처리 가능
- typeof 로 자료형을 확인하여 type narrowing
- in 으로 객체에 특정 키가 존재하는지 확인하여, 타입을 체크
제네릭
- 다양한 타입에 대응하기 위한 도구
- 타입만 다르고 비슷한 작업을 하는 것을 제너릭을 사용하여 간결하게 작성 가능
// 제네릭 T 를 사용하여 인수에 따라 다른 결과 도출 가능
function getFirstAndLast<T>(list: T[]): [T, T] {
return [list[0], list[list.length - 1]];
}
const [first, last] = getFirstAndLast([1, 2, 3, 4, 5]);
const [firstStr, lastStr] = getFirstAndLast(["a", "b", "c", "d", "e"]);
// multiple 제네릭 사용
function multipleGeneric<First, Last>(a: First, b: Last): [Fisrt, Last] {
return [a, b];
}
const [a, b] = multipleGeneric<string, boolean>("true", true);
인덱스 시그니쳐
- 객체의 키에 원하는 타입을 부여하는 방법
- 동적인 객체를 정의할 때 유용하지만, 존재하지 않는 키로 접근하면 undefined 를 반환하여 문제를 발생
- 객체의 키를 동적으로 선언되는 경우를 지양하고, 객체의 타입도 필요에 따라 좁혀야 한다
type Hello = Record<"hello" | "hi", string>;
const hello: Hello = {
hello: "hello",
hi: "hi",
};
type Hello = { [key in "hello" | "hi"]: string };
const hello: Hello = {
hello: "hello",
hi: "hi",
};
- Record 는 문자열 리터럴을 Key 로 사용 가능
type humanInfo = {
[name: "홍길동" | "둘리" | "마이콜"]: number;
};
// 위의 타입은 에러 발생
type names = "홍길동" | "둘리" | "마이콜";
type humanInfoRecord = Record<names, number>;
let human: humanInfoRecord = {
홍길동: 20,
둘리: 30,
마이콜: 40,
};
// Key 값을 문자열 리터럴로 지정 가능
덕 타이핑
- 어떤 것이 오리처럼 걷고, 헤엄치고, 소리를 내면 그건 오리이다!
- 자바스크립트의 특징으로 객체의 타입이 클래스 상속, 인터페이스 구현 등으로 결정되는 것이 아니다
- 어떤 객체가 필요한 변수와 메서드만 가지고 있다면 해당 타입에 속하는 것 처럼 인정해 주는 것을 말한다
- 따라서 타입 스크립트 역시, 타입 체크를 할 때 단순히 값이 가진 형태만 체크를 한다
- Car 타입으로 선언이 안되어도, 변수와 메서드만 가지고 있으면 문제 없이 실행
type Car = { name: string };
type Truck = Car & { power: number };
function horn(car: Car) {
console.log(`${car.name}가 경적을 울립니다!`);
}
const truck: Truck = {
name: "포드",
power: 100,
};
horn(truck); // 정상 작동
const obj = {
name: "오브젝트",
};
horn(obj); // 정상 작동
const obj2 = {
test: "테스트",
};
horn(obj2); // name 프로퍼티 없어서, ts 에러 발생
1.7.3 타입스크립트 전환 가이드
tsconfig.json 먼저 작성하기
JSDoc 과 @ts-check 를 활용해 점진적으로 전환하기
- JSDoc 주석을 사용하여 타입을 미리 명시적으로 확인하기
- // @ts-check 구문으로 미리 타입 확인
// @ts-check
/**
* @param {number} a
* @param {number} b
* @return {number}
*/
function sum(a, b) {
return a + b;
}
타입 기반 라이브러리 사용을 위해 @types 모듈 설치
- 타입스크립트로 작성되지 않은 코드에 대한 타입을 제공하는 라이브러리
- js 파일을 ts 로 전환시, 특정 라이브러리가 import 되지 않는 경우 @types 라이브러리를 설치하여 해결
파일 단위로 조금씩 전환하기
- 별도의 의존성이 없는 파일부터 ts 로 전환
- 전환 과정에서 잠재적 버그를 발견 -> 더 좋은 코드로 수정 가능