01장 리액트 개발을 위해 꼭 알아야 할 자바스크립트
JS for React
- 들어가며
- 1.1 자바스크립트의 동등비교
- 1.2 함수
- 1.3 클래스
- 1.4 클로저
- 1.5 이벤트 루프와 비동기 통신의 이해
- 1.6 리액트에서 자주 사용하는 자바스크립트 문법
- 1.7 선택이 아닌 필수, 타스
들어가며
왜 리액트인가
- 최근에 전자정부 표준 framework로 채택 → 안정성, 유지보수성 확보
- 명시적 상태변경 , 단방향 바인딩: component change s→ view changes
- if 상태 변경 → 상태 변화를 명시적으로 일으킨 함수만 찾으면 된다 (데이터 흐름의 변화가 단순)
- 반대방향도 가능하면 Angular
- 간단함, 유연함
- JSX, 간결함, 넓은 커뮤니티 등
1.1 자바스크립트의 동등비교
→ props의 동등비교 (which triggers rerendering) 은 얕은 비교이다
JS Datatypes
원시 (Primitive) 타입: 객체 제외 7개
boolean
null
: 명시적으로 비어 있는 값 (JS 초기 오류로 인해 typeof는 ‘object’이다..)undefined
: 선언 후 값 없는 인수에 자동 할당되는 값number
: 정수, 소수 다 가능한 \([-2^{53}-1, 2^{53}-1]\)(typeof
number ≠typeof
bigint)string
: 백틱 사용 시 줄바꿈도 가능 (원시 타입이기에 부분변경 안됨 ex: foo[0] = ‘a’ X)bigint
: 2020에 출시된 숫자 뒤에 ‘n’붙이거나 BigInt(..)로 사용, $2^{53}-1$보다 더 크기 가능symbol
: 중복되지 않은 고유의 값, 새로 추가됨!
객체 타입 (Object, Reference Type) :
object
: 배열, 함수, 정규식, 클래스 etc
원시vs객체 : 값을 저장하는 방식의 차이
- 원시 타입: 불변형태, 할당 시 메모리 영역 차지 → 저장
- 객체 타입: 변경가능한 형태로 저장 (CRUD), 복사 시 값이 아닌 참조를 전달 (그래서 얕은 복사하면 같은 참조값 바라봄..)
Object.is
==
(양쪽 타입 알아서 맞춰놓고 비교)보다 확실히 비교 (type 비교도 같이)===
보다 직관적으로 비교-0 === +0
→ trueObject.is(-0, 0)
→ false
- 하지만 객체 비교에는 도움이 되지 않음
Object.is({}, {})
→ false
리액트에서의 동등 비교 = Object.is
임 (objectIs(x, y)
)
but
Object.is
는 ES6기능이라 polyfil 함께 사용typeof Object.is === 'function' ? Object.is : polyfillIs
React는
shallowEqual
(usingobjectIs
) 로 객체의 1 depth까지만 확인- prop 에 따라 리렌더링 하기 때문 !
- so prop 안에 또 객체 넣으면
React.memo
가 정신 못 차릴 떄 있음 - recursive 하게 찾으면 안되나? => 성능저하 큰일남
객체 비교의 불완전성 - JS 근본없다고 하는 이유 중 하나! (unlike 하스켈, 스칼라..)
1.2 함수
4 Types of Functions
- 함수 선언문
- 표현식
- Function 생성자 (
const add = new Function('a', 'b', 'return a+b')
) : 미사용! - 화살표 함수 (
ES6
)
- JS 엔진 : 선언문을 표현식처럼 해석할 수도 있음.. (문맥에 따라)
함수 선언문 (Declaration) | 함수 표현식 (Expression) | |
---|---|---|
식 | function add(a, b) {} | const add = function(){} |
호이스팅 | 코드의 순서없이 메모리에 함수 등록 | (변수등록) hoisting 하지만 undefined 먼저 등록 |
when to use | 어디서든 자유롭게 호출, 명시적으로 작성하고 싶을 때 | 관리해야 하는 스코프가 있을 때 |
표현식: 일급 객체여서 가능
- 다른 함수의 매개변수 가능
- 반환값 가능
- 변수에 할당 가능
hoisting: 함수에 대한 선언을 실행 전에 메모리에 등록하는 작엄
화살표 함수
constructor
사용 불가const Car = (name) => { this.name = name; }; const myCar = new Car("BMW"); // TypeError
no arguments
function hello() { console.log(arguments); } // hello(1,2,3) 하면 출력함 const hi = () => { console.log(arguments); }; // hi(1,2,3) 하면 ReferenceError
this
binding (자신이 속한 객체 or 생성할 인스턴스 가리키는 값)- 원래: 함수 호출에 따라 동적으로 결정 됨 (예: 일반함수 호출 -> 내부의 this = 전역 객체)
in 화살표 함수: 함수 자체의 바인딩을 갖지 않음. 내부에서
this
참조 시, 상위 스코프의this
따름class Component extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } functionCounter() { console.log(this); // undefined (객체 내부를 의미) this.setState({ count: this.state.count + 1 }); } arrowCounter = () => { console.log(this); // class Component (상위 scope의 this를 따름) this.setState({ count: this.state.count + 1 }); }; render() { return ( <> {/** cannot read property of undefined'setState' of undefined */} <button onClick={this.functionCounter}>Function</button> {/** 정상작동 */} <button onClick={this.arrowCounter}>Arrow</button> </> ); } }
- babel 트랜스파일링에서도 확인 가능
- 화살표 함수 :
this
선언 시점에 (상위 스코프로) 결정, 받아 사용 가능 - 일반 함수: 호출 시 런타임에 결정
Various Functions
즉시 실행 함수 (
IIFE
: Immediately Invoked Function Expression), 재호출 불가능((a, b) => { console.log(a + b); })(1, 2);
고차 함수 (일급객체라는 특징을 활용) -> 고차 컴포넌트도 가능
- 함수를 인자로 받거나 함수를 반환하는 함수
map
,filter
,reduce
등
const add = (a) => (b) => a + b; const add(10)(20); // 30 const add10 = add(10);
- 부수효과 줄일 것 -> 완전히는 불가능 하지만 최대한 퓨어하게~
- 가능한 작게 함수 만들 것 (코드 냄새 피하기, 재사용성 높이기)
1.3 클래스
- JS 에서 모든 클래스는 함수로 표현 가능
- 인스턴스 메서드 - 클래스 내부에 선언, aka 프로토타입 메서드
- prototype에 선언됨, 고로 prototype chain을 따라가면 찾을 수 있음
- 정적 메서드 - 클래스 자체에 선언 (인스턴스가 아닌, 이름으로 호출)
this.state
에 접근 불가
- 클래스는 ES6부터 지원, 이전에는 프로토타입 활용해 클래스의 작동 방식 구현 가능
- 클래스는 일종의 문법적 설탕 (syntactic sugar)
- JS 클래스는 프로토타입 기반으로 작동!
1.4 클로저
- JS FC 이해에 핵심적인 개념 (구조, 작동방식, 훅의 원리, [deps] 등)
클로저의 정의
Closure : 함수 + 함수 선언된 Lexcial Scope
- 선언된 어휘적 환경 : 동적 (언제 호출되었는지,
this
)가 아닌, 정적 (어디서 선언되었는지)으로 결정
변수의 유효범위, 스코프
전역 스코프: 어디서든 접근 가능
- 전역 스코프는 전역 객체에 바인딩 됨 (
window
in Browser,global
in Node)
- 전역 스코프는 전역 객체에 바인딩 됨 (
함수 스코프
다른 언어들과 달리 블록레벨 (
{ }
) 따르지 않음if (true) { var global =’global scope’ } console.log(global) // ’global scope’ console.log(global === window.global) // true
- 블록 레벨이었다면
ReferenceError
- 블록 레벨이었다면
_클로저의 활용
ex:
useState
: 리액트가 별도로 관리하는 클로저 내부에서만 접근 가능한 상태값- 활용 시 전역 스코프 사용 막을 수 있음
- 개발자가 원하는 정보만 공개 가능
function Component() {
const [count, setCount] = useState(0);
function onClick() {
setCount((prev) => prev + 1);
}
}
클로저가 useState 내부에서 활용되었기 때문에 호출이 끝나도 prev 값을 안다??
- 외부함수 (useState)가 종료되어도 내부함수 (setCount)가 참조하는 변수는 사라지지 않음
주의점
- 클로저 사용 시 비용 발생 (선언적 환경을 기억해야하기 때문)
긴 작업을 일반적인 함수로 처리:
const aButton = document.getElementById("a"); function heavyJob() { const longArr = Array.from({ length: 10000000 }, (_, i) => i + 1); console.log(longArr.length); } aButton.addEventListener("click", heavyJob);
클로저로 처리:
const aButton = document.getElementById("a"); function heavyJobWithClosure() { const longArr = Array.from({ length: 10000000 }, (_, i) => i + 1); return function () { console.log(longArr.length); }; } const innerFunc = heavyJobWithClosure(); aButton.addEventListener("click", () => { innerFunc(); });
1.5 이벤트 루프와 비동기 통신의 이해
- 동기 - (synch) 직렬
- 이 요청이 시작된 이후에는 무조건 응답을 받은 이후에야 다른 작업을 처리할 수 있음
- \(\rightarrow\)
JS 기본적으로 동기 적
- 비동기 - (async), 병렬
- 요청 시작 후 응답 여부와 상관없이 다음 작업이 이루어지므로 한 번에 여러 작업이 실행될 수 있음
- JS 비동기도 가능
- 근데 사실 병렬이 아니라 걍 기다리지 않고 넘어가는 것!
싱글 스레드 자바스크립트
why single-threaded?
- 초기 의도: HTML 보조역할
- if multithreading happens -> race condition (경쟁 상태) 발생 가능
비동기 코드 처리 시 오래걸리는 작업 (fetching, reading files etc.) 별도로 처리,
- 작업 완료 시 콜백함수
- 이벤트 루프 사용! (효율 증대)
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
setTimeout(() => {
console.log(3);
}, 100);
console.log(4);
- 답은
1 2 3 4
가 아닌,1 4 2 3
임
𐃏 Process : 실행 단위
🪡 Thread: 프로세스보다 더 작은 실행 단위하며 메모리를 공유해 여러 작업 동시 수행 가능
- 옛날: 1 process, 1 work
- 현재: 하나의 프로세스 -> 여러개 스레드
이벤트 루프
설명 기준: V8 (딱히 ECMAScript에는 없음)
- JS를 런타임 외부에서 비동기 작업 실행 관리 위해 만들어짐
- JS 런타임 : JS 실행환경 (browser, nodejs)
Medium: JavaScript Event Loop & its functions
- 📚 Call Stack: 자바스크립트에서 실행할 코드나 함수를 순차적으로 담아두는 Stack
- Callback/Task Queue: 비동기 함수의 콜백 / 이벤트 핸들러 담기는 곳
- 🔄 Event Loop: 호출 스택이 비어 있는지 확인하고, 비어 있을 경우 큐에서 대기 중인 작업을 실행 가장 오래된 애부터 순차적으로 꺼내서 실행
function bar() {
console.log("bar");
}
function baz() {
console.log("baz");
}
function foo() {
console.log("foo");
bar();
baz();
}
foo();
CALL STACK기준:
- [
foo()
] - [
foo()
,console.log
] - [
foo()
]// console log 실행
- [
foo()
,bar()
] - [
foo()
,bar()
,console.log
] - [
foo()
,bar()
]// console log 실행
- [
foo()
] (bar
에 더 이상 없음 -> 제거) - [
foo()
,baz()
] - [
foo()
,baz()
,console.log
] - [
foo()
,baz()
]// console log 실행
- [
foo()
] (baz
에 더 이상 없음 -> 제거) - [] (
foo
에 더 이상 없음 -> 제거) - 끗! -> 이벤트 루프가 확인함
그래서 동시는 불가능하고, 순차적으로 함
function bar() {
console.log("bar");
}
function baz() {
console.log("baz");
}
function foo() {
console.log("foo");
setTimeout(bar, 0);
baz();
}
foo();
CALL STACK - TASK QUEUE
- [
foo()
] - [
foo()
,console.log
] - [
foo()
] - [
foo()
,setTimeout(bar, 0)
] - [
foo()
] - [setTimeout(bar, 0)
] - [
foo()
,baz()
] - [setTimeout(bar, 0)
] - [
foo()
,baz()
,console.log
] - [setTimeout(bar, 0)
] - [
foo()
,baz()
] - [setTimeout(bar, 0)
] - [
foo()
] - [setTimeout(bar, 0)
] - [] - [
setTimeout(bar, 0)
] - 끗 (이벤트 루프가 확인)
- [
bar()
] - [] - [
bar()
,console.log
] - [
bar()
] - []
- \(\Rightarrow\) 딱히 0초 뒤에 실행된다는 것을 보장해주지는 않음!!!
- 원래 큐: FIFO
- 우리 (테스크) 큐: FIFO 아님
- 이런 비동기 함수 실행: 메인 스레드가 아니라 별도의 스레드에서 수행됨!!
- 브라우저나 노드js가 실행
- => JS 는 싱글스레드, 근데 외부 도움 필요한 언어
Task Queue, Micro Task Queue
이벤트 루프는 하나의 마이크로 태스크 큐를 가짐
- 태스크 큐: [
setTimeout
,setInterval
,setImmediate
] - 마이크로 태스크 큐: [
process.nextTick
,Promises
,queueMicroTask
,MutationObserver
]
- 태스크 큐: [
- 마이크로 테스크 큐가 우선권 보유 (얘가 비어야만 테스크 큐 작업 가능)
마이크로 태스크 큐가 빌 때까지는 기존 태스크 큐 실행은 뒤로 미루어짐
- 마이크로 태스크 큐 → 브라우저 렌더링 → 태스크 큐
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
1.6 리액트에서 자주 사용하는 자바스크립트 문법
- Babel: transpiles JS
- 최신문법을 다양한 브라우저에서도 일관적으로 지원가능토록
구조 분해 할당
Array destructuring
const arr = [1, 2, 3, 4, 5]; const [first, second, ...arrayRest] = array; // arrayRest: [3, 4, 5] const [first, , , , fifth] = array; const array2 = [1, 2]; const [a = 10, b = 10, c = 10] = array2; //a: 1, b: 2, c: 10
- in babel transplilation:
const array = [1, 2, 3, 4, 5]; //before const [first, second, ...arrayRest] = array; //after var first = array[0], second = array[1], arrayRest = array.slice(2);
Object Destructuring
const object = { a: 1, b: 1, c: 1, d: 1, e: 1, };
Babel Transpilation Before & After
```js //before const { a, b, …rest } = object
//after function _objectWithoutProperties(source, excluded){ if(source==null) return {} var target = _objectWithoutPropertiesLoose(source, excluded) var key, i; if(Object.getOwnPropertySymbols){ var sourceSymbolKeys = Object.getOwnPropertySymbols(source) for(i=0; i<sourceSymbolKeys.length; i++){ key = sourceSymbolKeys[i] if(excluded.indexOf(key)>=0) continue if(!Object.prototype.propertyIsEnumerable.call(source, key)) continue target[key] = source[key] } } return target } function _objectWithoutPropertiesLoose(source, excluded){ if(source==null) return {} var target = {} var sourceKeys = Object.keys(source) var key, i; for(i=0; i<sourceKeys.length; i++){ key = sourceKeys[i] if(excluded.indexOf(key)>=0) continue target[key] = source[key] } return target } var object = { a: 1, b: 1, c: 1, d: 1, e: 1, } var a = object.a, b=object.b, rest=\_objectWithoutProperties(object, ['a','b']) ```
전개 구문 (spread syntax)
- 순회할 수 있는 값 (ex: 배열, 객체)에 대해 그대로 전개해 간결하게 사용가능케 함
- 구조분해 할당과 마찬가지로 전개 트랜스파일 시 번들링 커지니 사용에 주의
Array Prototype Methods
map
,filter
,reduce
,forEach
(Array.prototype._)- 배열 값 더럽히지 않고 새로 만들어 사용하기에 안전!
- ES5부터 사용 -> 별도의 트랜스파일/폴리필 불피요
filter
: callback 인수로 받음 (truthy 할 때만 원소 반환 )const arr = [1, 2, 3, 4, 5]; const evenArr = arr.filter((item) => item % 2 === 0);
reduce
: callback, 초기값const sum = arr.reduce((current, iterator) => { return current + item; }, 0); // <-- 초기값!!!
map
: callback + 반환함forEach
: 반환값 없음 (callback만 실행!)- 멈추기 불가능 (unless throw error or stop process)
break
,return
다 안된다..!- WHY? forEach 내 return: 함수의 Return X, 콜백의 return임!!
1.7 선택이 아닌 필수, 타스
use
unknown
instead ofany
😤 (정적 타이핑 사용!)
unknown
: 모든 값을 할당할 수 있는 top typeany
처럼 바로 사용하지는 못 함type narrowing 필요
function doSomething(callback: unkown) { if (typeof callback === "function") { callback(); } throw new Error(".."); }
never
: 어떤 타입도 허용 안하는 bottom typetype guard를 적극 활용하자
instanceof
: 특정 클래스의 인스턴스인지 확인typeof
: 특정 요소에 대해 자료형 확인in
: 객체에 키가 존재하는지
타입 사용처에서는 좁을수록 좋다
Generic
- 다양한 타입의 데이터를 사용 가능
- \(\rightarrow\) 코드의 재사용성과 타입 안전성을 높이기
함수에서의 제네릭 (arg, ret 값 모두)
function identity<T>(arg: T): T { return arg; } let output1 = identity < string > "myString"; // 타입이 string으로 지정 let output2 = identity < number > 100; // 타입이 number로 지정
인터페이스 제네릭
interface GenericIdentityFn<T> { (arg: T): T; }
클래스 제네릭
class GenericNumber<T> { zeroValue: T; add: (x: T, y: T) => T; } let myGenericNumber = new GenericNumber<number>();
인덱스 시그니쳐
객체의 키를 정의하는 방식
interface Dictionary<T> { [key: number]: T; } let keys: keyof Dictionary<number>; // 숫자 let value: Dictionary<number>['foo']; // 오류, 프로퍼티 'foo'는 타입 'Dictionary<number>'에 존재하지 않습니다. let value: Dictionary<number>[42]; // 숫자 // Record<K,T> interface Dictionary<T> = Record<number, T>
Record<K,T> : 객체의 타입 줄이기
type Hello = Record<"hello" | "hi", string>; // 는 아래와 같음 type Hello2 = {[key in 'hello' | 'hi']: string}
인덱스 시그니처의 문제점
- 단독 사용 시 (index signature ONLY) 빈 객체 할당해도 에러가 나지 않음
type ArrStr = { [key: string]: string | number, [index: number]: string, }; const a: ArrStr = {}; // 타입 선언 a["str"]; // 에러 x
- 유연한 대신 타입 안전성을 잃음 (키 이름 잘못 쓰는 둥 휴먼 에러)
- Index signature는 런타임에 객체의 프로퍼티를 알 수 없는 경우에만 사용할 것