모던 리액트 deep dive 스터디 - 1주차 발표 정리
리액트를 선호하는 이유는 뭘까?
- 명시적인 상태 변경
리액트의 상태 변화는 '단방향' => 명시적이다.
양방향 데이터 흐름이 나쁜게 아니다. 다만, 단방향의 데이터 흐름은 변화가 단순하기 때문에 코드를 읽기가 쉽고 버그를 야기할 가능성이 비교적 적다는 장점이 있다.
- JSX (JavaScript XML)
Html에 JavaScript 문법을 더하다.
고유의 몇가지 특징만 이해하면 손쉽게 구현할 수 있다. ex) null은 아무것도 렌더링하지 않는다, JS 문법은 {}로 감싸야한다.
참고로, JSX는 리액트에 종속적이지 않은 독자적인 문법이다.
- 비교적 배우기 쉽고 간결하다.
기존에 Html, JavaScript를 써봤다면, 쉽게 배우고 쓸 수 있다.
(개발 초기에는 쉬울지몰라도, 성능 최적화 부분은 상대적으로 어려운 편)
- ⭐️ 강력한 커뮤니티
가장 최고의 장점일 수도? 현시점에서 가장 많은 사람들이 웹 개발할 때 사용하고, 많은 이슈와 많은 문제 해결 공유가 이뤄진다.
1장. 리액트 개발을 위해 꼭 알아야 하는 자바스크립트
자바스크립트의 데이터 타입
원시 타입 (객체가 아닌 모든 타입)
- null
(typeof null은 object다, 자바스크립트 초창기 값 표현 방식에 대한 문제)
- undefined
- boolean
- number
- string
- symbol
- bigint
객체 타입
- object
❓헷갈릴만한 것들
1.
undefined => 선언됐지만 할당되지 않은 값
null => 명시적으로 비어 있음을 나타내는 값
2.
falsy 한 값
- false
- 0, -0, 0n, 0x0n
- NaN
- '', "", ``
- null
- undefined
여기서 문자열이 falsy하기 위해서는 반드시 공백이 없는 빈 문자열이어야 한다.
truthy 한 값
- falsy 한 값 이외에 모두
ex) ' ', {}, [], 1...
3.
문자열은 원시 타입이며, 이는 변경 불가능 하다.
const foo = 'bar';
foo[0] = 'f'; // JavaScript 엔진은 문자열이 불변임을 인식하고, 할당 작업을 무시한다.
console.log(foo); // bar
4. Symbol
const key = Symbol('key');
const key2 = Symbol('key');
console.log(key === key2); // false
console.log(Symbol.for(key) === Symbol.for(key2)); // true
const obj = {};
const secret = Symbol("secretKey");
obj[secret] = "hiddenValue";
console.log(obj[secret]); // 'hiddenValue'
console.log(Object.keys(obj));
⭐️ 객체 타입
7가지 원시 타입 이외의 모든 것, 자바스크립트를 이루고 있는 대부분의 타입 (배열, 함수, 정규식, 클래스)
typeof [] === 'object' // true
typeof {} === 'object' // true
원시 타입과 객체 타입의 가장 큰 차이점은 값을 저장하는 방식이다.
원시 타입은 값을 직접 저장하지만, 객체 타입은 메모리 상에서 참조된다. (변수에는 객체의 위치가 저장됨)
원시 타입은 값이 불변하여, 변경하려면 변수에 재할당해야한다. 또한, 속성이나 메서드가 없다.
동등 비교시에 원시 타입은 값 자체를 비교한다.
객체 타입은 참조값을 비교한다. (복사시에도 값이 아닌 참조를 전달)
객체는 값이 같더라도 서로 다른 참조를 갖기 때문에 동등비교시 false
❓ 동등비교
📌 Object.is
== 와 === 의 차이는 강제 형변환을 시키는지에 대한 부분
Object.is는 이러한 === 보다 더 개발자가 기대하는 방식으로 더 정확히 비교
ex)
-0 === +0 // true
Object.is(-0, +0) // false
Number.NaN === NaN // false
Object.is(Number.NaN, NaN) // true
NaN === 0 / 0 // false
Object.is(NaN, 0/0) // true
=== 보다 더 정확해진 문법이지만, 객체 비교에 있어서는 ===와 동일하게 동작함.
Object.is({}, {}); // false
const a = {
hello: "hi",
};
const b = a;
b["hello"] = null;
console.log(Object.is(a, b)); // true
console.log(a === b); // true
console.log(a == b); // true
Object.is 개념이 중요한 이유 => 리액트에서 사용하는 동등 비교의 방법이기 때문.
리액트에서는 객체 간 얕은 비교만 이뤄짐 (첫 번째 깊이에 대한 값)
리액트에서의 비교는 어떨때 이뤄지는데? => props 비교
리액트의 렌더링 조건에는 props의 변화가 있다.
리액트는 이 props의 변화를 Object.is의 수행 결과를 통해 알게된다.
Object.is는 객체의 얕은 비교만을 수행하는데? 왜 깊은 비교는 안하고....?
1. 성능 향상
깊은 비교(객체 내부의 모든 값 확인)는 재귀적으로 탐색하면서 값을 비교해야 하기 때문에 연산 비용이 크다. 하지만 얕은 비교는 참조 값만 비교하므로 비교 속도가 훨씬 빨라 성능면에서 이득이다.
2. 일반적인 사용 사례
React에서 사용하는 props의 대부분은 원시값이거나 얕은 객체일 가능성이 높다. 대부분의 상황에서 커버가 된다.
3. React의 불변성 원칙에 적합
리액트는 상태나 props가 변경될 때, 깊은 수정 대신 새로운 객체를 생성하여 변경 사항을 전달하도록 권장한다.
이렇게 하면 얕은 비교만으로도 값이 변경되었는지 쉽게 감지할 수 있다.
예를 들어, 부모 컴포넌트에서 props가 변경되면 새로운 참조를 가지게 되므로 얕은 비교로도 해당 변경 사항을 인지할 수 있다.
❓ 얕은 객체
객체가 다른 객체를 속성으로 가지지만, 그 속성들이 또 다른 객체를 참조하는 중첩 객체가 아닌 경우. 즉, 속성이 원시값이거나 또 다른 객체를 가지지 않는 경우
=> JSON.stringfy(JSON.parse()) 보다는 structuredclone
JSON.stringify와 JSON.parse는 Date, Set, Map, undefined, Infinity, NaN, RegExp, BigInt, Symbol과 같은 JavaScript의 다양한 데이터 타입을 제대로 처리하지 못한다.
ex) Date 객체는 문자열로 변환, undefined나 Infinity와 같은 값은 복사에서 제외
반면, structuredClone()은 이러한 다양한 데이터 유형을 그대로 복사할 수 있다.
const date = new Date();
console.log(typeof JSON.parse(JSON.stringify(date))); // string
console.log(typeof structuredClone(date)); // object
📌 함수
함수 선언문
function add(a,b) {
return a + b;
}
함수 표현식
const add = function(a, b) {
return a + b;
}
자바스크립트에서 함수는 일급 객체이다.
일급 객체는 값으로 다뤄질 수 있으므로 함수 또한 변수에 할당 가능하다.
함수 표현식과 함수 선언문의 차이 => 호이스팅
❓ 호이스팅
JS 엔진은 모든 선언문을 소스 코드의 위치에 상관없이 다른 코드들보다 먼저 실행한다. 런타임 이전에 실행 컨텍스트에 의해 소스코드 평가 과정에서 스코프에 등록되어 마치 코드의 제일 상단으로 끌어올려 진 것과 같은 효과를 불러와서 변수가 어데 선언 되어있든 변수를 참조할 수 있게됨. var, let, const 뿐만 아니라 function, class 키워드를 사용하는 모든 식별자들은 호이스팅이 된다.
실행 컨텍스트 생성 시 렉시컬 스코프 내의 선언이 끌어올려 지는 게 호이스팅이다
둘 다 호이스팅 되는거 아닌가?
- 함수 선언문은 해당하는 함수의 전체 내용이 호이스팅된다. 따라서, 어디서든 호출 가능
- 함수 표현식은 함수를 변수에 할당하는 것인데, 이 변수 선언 부분만 호이스팅 되고 함수 할당은 호이스팅되지 않음. 따라서 함수 호출은 함수 할당 이후에만 가능
화살표 함수의 특징
- constructor를 사용할 수 없다 => 생성자 함수로 사용 불가
- arguments가 존재하지 않는다.
- 일반함수와 this 바인딩의 차이
일반 함수 내부의 this는 전역 객체를 가리킨다.
화살표 내부의 this는 상위 스코프의 this를 따르게 된다.
즉시 실행 함수
((a, b) => {
return a + b
},
)(5, 10) // 15
딱, 한 번만 실행하는 함수. (ex. 페이지 렌더링 시 초기 API 부르는 용도)
고차 함수
자바스크립트에서 함수는 일급 객체로 취급 받는데, 이러한 함수를 인자로 받아 결과를 새로운 함수로 반환 시킬수 있는 함수를 말한다.
ex) map, filter, reduce...
‼️ 함수 만들때 주의 사항
- 함수의 부수 효과를 최대한 억제하라.
- 가능한 한 함수를 작게 만들어라. 함수는 한 가지 일만 하는 것이 좋다.
- 누구나 이해할 수 있는 이름을 붙여라
클래스
특정한 객체를 만들기 위한 일종의 템플릿과 같은 개념.
자바스크립트 클래스는 ES6에서 출시
자바스크립트 클래스는 프로토타입을 기반으로 작동
constructor
객체를 생성하는 데 사용하는 특수한 메서드, 단 하나만 존재 가능 (생략 가능)
프로퍼티
클래스로 인스턴스를 생성할 때 내부에 정의할 수 있는 속성값
getter와 setter
getter는 무언가 값을 가져올 때, setter는 클래스 필드에 값을 할당 할 때
인스턴스 메서드
클래스 내부에 선언한 메서드, prototype에 선언되므로 프로토타입 메서드라고 부르기도 함.
이 덕분에 자기 자신부터 시작해서 최상위 객체인 Object까지 흝을 수 있는 프로토타입 체이닝을 수행할 수 있어 선언하지 않는 곳에서도 호출이 가능하다.
정적 메서드
클래스 인스턴스가 아닌 이름으로 호출할 수 있는 메서드
실무에서 클래스를 사용한 사례
ex) 엑셀 builder 시 사소한 작업들이 많다. CellBuilder라는 클래스 선언 후 내부에 인스턴스 메서드로 다양한 작업 수행.
가독성면에서 좋음.
⭐️ 클로저 => useState
함수와 함수가 선언된 어휘적인 환경. => 개념이 어렵다.
어휘적인 환경은 변수와 함수가 정의된 위치에 따라 결정되는 실행 컨텍스트의 일부.
각 함수는 자신이 선언된 어휘적인 환경을 가지며, 함수가 실행될 때 이 환경을 기반으로 변수에 접근
기본적으로 자바스크립트는 함수 레벨 스코프를 가짐.
클로저를 통해 함수는 자신을 감싸고 있는 스코프의 변수에 접근하고 변경할 수 있음.
어디서 호출 했는지에 따른 것이 아니라, 어디에 선언되었는지에 따라 달라진다.
리액트가 관리하는 내부 상태 값은 리액트가 별도로 관리하는 클로저 내부에서만 접근 가능하다.
클로저의 원리가 사용되는 곳이 바로 useState
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i)
}, i * 1000)
}
i는 전역 변수로 작동하기때문에 5만 출력됨.
함수 레벨 스코프를 가지는 var가 아닌 블록 레벨 스코프를 가지는 let으로 선언하자.
클로저는 선언적 환경을 기억하므로 당연히 그에 따른 비용이 든다는 것을 잊지말자.
이벤트 루프와 비동기 통신
자바스크립트는 싱글 스레드에서 동기 방식으로 동작한다.
하지만, 실제로 비동기 작업도 많이 수행되는데 어떻게 가능한것인지?
그것보다 우선 자바스크립트는 왜 싱글 스레드로 구현되었을까?
JS는 초기에 HTML을 그리는 데 한정적인 도움을 주기는 보조적인 역할로 만들어짐.
단순한 작업을 처리하기에는 싱글 스레드가 오히려 적합했다.
이벤트 루프
호출 스택 : 자바스크립트에서 수행해야 할 코드나 함수를 순차적으로 담아두는 스택
이벤트 루프는 이 호출 스택이 비어 있는지의 여부를 확인하는 것을 말한다.
태스크 큐 : 실행해야 할 태스크(비동기 함수의 콜백 함수나 이벤트 핸들러 등)의 집합, 큐가 아닌 set의 형태
이는 별도의 스레드에서 이뤄짐. 엥? JS는 싱글 스레드라고 하지 않았나?
JS 코드 실행은 싱글 스레드에서 이루어지지만,
태스크 큐에 작업을 할당하는 브라우저나 Node.js 와 같은 외부 Web API 등은 JS 코드 외부에서 실행되고 콜백이 테스크 큐로 들어감.
이때, 이벤트 루프가 호출 스택이 비고 콜백이 실행 가능한 때가 오면 이것을 꺼내 수행하는 역할을 하는 것
마이크로 태스크 큐 : 태스크 큐보다 우선순위가 높다. ex) Promise
따라서, Promise는 setTimeout보다 먼저 실행된다.
렌더링은 언제 수행될까? => 마이크로 태스크 큐를 실행한 뒤에 일어난다. 마이크로 태스크 큐와 태스크 큐 사이 단계.
리액트에서 자주 쓰는 JS 문법
⭐️ 구조 분해 할당
const [1, 2, ...rest] = numArr
const [a = 1, b = 1, c = 1] = [undefined, null]
console.log(a, b, c) // 1, null, 1
- undefined 일 때만 기본값을 사용
const {a, b, ...rest} = object
- API 호출 시 body나 params를 넘길 때 매우 자주 사용
- 마지막에 작성해야 한다.
전개구문
전개구문을 통한 복사
const names = [{ name: 'A' }, { name: 'B' }];
const copiedNames = [...names];
copiedNames[0].name = 'C';
console.log(anotherNames); // [ { name: 'C' }, { name: 'B' } ]
- 전개 구문 복사는 얕은 복사를 수행
- 배열 내부에 있는 객체나 배열은 참조를 복사하므로 내부 객체나 배열의 변경이 이뤄지면 복제된 결과에 영향을 줌
structuredClone을 통한 복사
const anotherNames = [{ name: 'A' }, { name: 'B' }];
const copiedAnotherNames = structuredClone(anotherNames)
copiedAnotherNames[0].name = 'C';
console.log(names); // [ { name: 'A' }, { name: 'B' } ]
- 완전 분리됨
Array 프로토타입의 메서드: map, filter, reduce, forEach
모두 자주 사용하는 메서드.
특히, map, filter는 리액트 요소를 반환할때 자주 사용한다.
map과 filter를 같이 사용할때는 reduce를 사용하면 된다. (다만, 가독성을 해칠 수 있다.)
forEach의 특징은 실행되는 순간 에러를 던지거나 프로세스를 종료하지 않는 이상 이를 멈출 수 없다.
배열에서 특정 값을 찾을 때 순회를 멈추고 싶다면, find를 써보자.
삼항 연산자
조건부 렌더링을 구현할 때 주로 쓰임, 중첩은 사용하지 말자.
타입스크립트
📌 any 대신 unknown
- any를 사용하면 타입스크립트의 이점을 잃는 것과 비슷하다.
- unknown을 대체제로 쓰자.
unknown => 아직 알 수 없는 값
never => 어떠한 타입도 들어올 수 없다, 코드상으로 존재가 불가능한 타입
- 타입 가드를 적극 활용
타입을 사용하는 쪽에서 최대한 줄이기
instaceof => 지정한 인스턴스가 특정 클래스의 인스턴스인지 확인
typeof => 특정 요소에 대해 자료형을 확인
in => 어떤 객체에 키가 존재하는지 확인
제네릭
- 다양한 타입을 처리할 때 유용하다.
- useState의 타입 선언부분이 대표적
인덱스 시그니처
Object.keys(items).map((item) => {
const value = items[item]
return value
}
string은 items의 index로 사용할 수 없다는 오류를 자주 접하게 될 것이다.
이는 Object.keys는 string[]으로 추론되기 때문.
자바스크립트는 덕 타이핑으로 객체를 비교해야하는 특징을 가짐
❓덕 타이핑
객체의 타입이 클래스 상속이 인터페이스 구현 등으로 결정되는 것이 아니고 어떤 객체가 필요한 변수와 메서드만 가지고 있다면 그냥 해당 타입에 속하도록 인정해주는 것.
모든 키가 들어올 수 있는 가능성이 열려 있는 객체의 키에 포괄적으로 대응하기 위해서다.
TypeScript는 컴파일 타임에 타입을 검사한다.
하지만 JavaScript는 동적 타입 언어로, 객체 속성은 런타임에 추가되거나 삭제될 수 있다.
즉, TypeScript는 코드가 실행되기 전인 컴파일 시점에는 객체에 어떤 속성이 있는지 완벽히 알 수 없다.
JavaScript 객체는 확장 가능하고 동적으로 속성을 추가할 수 있기 때문이기도 하다.
const person = { name: "Alice" };
person["age"] = 30; // 런타임에서 속성 추가
차라리, string[]으로 추론하는 것이 더 안전한 선택이다.
*키를 단언하는 법
Object.keys(items).map((item) => {
const value = items[item as keyof Items]
return value
}
'프로그래밍언어 > React' 카테고리의 다른 글
모던 리액트 deep dive 스터디 - 4주차 발표 정리 (0) | 2024.03.30 |
---|---|
모던 리액트 deep dive 스터디 - 3주차 발표 정리 (1) | 2024.03.22 |
React 잘못된 경로 페이지 만들기 (Router) (0) | 2022.06.07 |
Object 를 Array로 바꾸기 (React, JS) (0) | 2022.06.07 |
React, Spring Boot연동 로그인 정보 불러오기 (0) | 2022.05.31 |