🙋 내가 기존에 생각하던 내용
1. 컴포넌트 기반 코드 >> 재사용성, 책임 분리
2. 화면 업데이트를 효율적으로 함 (Virtual DOM)
3. 선언적이므로 화면 업데이트를 쉽게 할 수 있다
당시 주류였던 양방향 바인딩 구조는 코드 작성은 간단하지만 변경된 DOM과 변경 이유를 추적하는 것이 어려웠다.
그래서 모델이 뷰를 변경하는 단방향 방식으로, 이전 DOM을 버리고 새로운 데이터에 따라 새로 렌더링하는 방식을 채택.
⭐️ 선언적 인터페이스를 통해 간단하게 작성하고, 필요하지 않은 영역에 대한 DOM 변경을 하지 않아 효율적
⭐️ 리액트는 시간의 흐름에 따라 변경되는 데이터를 효율적으로 나타내기 위한 재사용 가능한 컴포넌트를 만드는 데 중점을 둔다.
1.1.1 자바스크립트의 데이터 타입
Truthy와 Falsy
- Falsy: 조건문에서 false로 취급
- false, 0, -0, NaN, “”, null, undefined
- Truthy: 조건문에서 true로 취급 - falsy 값 이외 모든 값
- 객체와 배열은 항상 truthy
1.1.2 값을 저장하는 방식의 차이
객체 간 비교 시 내부의 값이 같더라도 true가 아닐 수 있다는 점을 인지해야 한다.
이는 메모리에 있는 주소 값이 다르기 때문이다.
원시 타입
참조 타입
🙋 그래서 참조 타입을 제대로 비교할 방법이 필요하다.
1.1.3 Object.is
ES6에서 새롭게 도입된 비교 문법
==
와 동등 비교 ===
가 만족하지 못하는 특이 케이스까지 고려하여 비교
Object.is와 ==
5 == "5"; // true
Object.is(5, "5"); // false
Object.is와 ===
-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
1.1.4 리액트에서의 동등 비교
아직도 헷갈리는 내용 ❓
shallowEqual 함수: Object.is로 먼저 비교하고, 객체 간 얕은 비교를 한 번 더 수행
코드 보기
// React의 shallowEqual.js
function shallowEqual(objA: mixed, objB: mixed): boolean {
// 객체 참조가 같은 경우 동일한 것으로 판단
if (is(objA, objB)) {
return true;
}
// 객체가 아니거나 null인 경우 비교하지 않도록
if (typeof objA !== "object" || objA === null || typeof objB !== "object" || objB === null) {
return false;
}
// A의 키 값과 B의 키 값을 비교
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
for (let i = 0; i < keysA.length; i++) {
const currentKey = keysA[i];
if (
!hasOwnProperty.call(objB, currentKey) ||
// $FlowFixMe[incompatible-use] lost refinement of `objB`
!is(objA[currentKey], objB[currentKey])
) {
return false;
}
}
return true;
}
객체의 첫 번째 깊이까지만 비교하는 이유는 객체 안에 객체가 몇 개까지 있을지 알 수 없으므로 성능 상 문제를 고려해야하며, 일반적으로 객체인 JSX props를 비교하기에 충분하기 때문이다.
따라서 props에 객체를 넘겨줄 경우, 리액트 렌더링이 예상치 못하게 동작할 수 있다. 예를 들어, 객체를 props로 넘겨받으면 React.memo는 항상 새로운 props를 받은 것으로 판단하여 메모이제이션된 컴포넌트를 반환하지 못하고, 따라서 리렌더링 된다.
부모 컴포넌트의 상태를 변경하여 리렌더링했을 때 Deeper Component만 리렌더링되고, Component는 정확히 객체 간 비교를 수행해 렌더링을 방지해주었다.
type Props = {
counter: number;
};
// 메모이제이션 O
const Component = memo((props: Props) => {
useEffect(() => {
console.log("Component has been rendered");
});
return <h1>{props.counter}</h1>;
});
type DeeperProps = {
counter: {
counter: number;
};
};
// 메모이제이션 X
const DeeperComponent = memo((props: DeeperProps) => {
useEffect(() => {
console.log("Deeper Component has been rendered");
});
return <h1>{props.counter.counter}</h1>;
});
const App = () => {
const [, setCounter] = useState(0);
function handleClick() {
setCounter((prev) => prev + 1);
}
return (
<div>
<Component counter={100} />
<DeeperComponent counter={{ counter: 100 }} />
<button type="button" onClick={handleClick}>
Add
</button>
</div>
);
};
자바스크립트에서 객체 비교는 불완전하다. 이러한 자바스크립트를 기반으로 리액트는 Object.is 비교 + 객체의 얕은 비교를 수행하여 상태의 변경을 감지한다.
함수형 컴포넌트에서 사용되는 훅의 의존성 배열 비교, 렌더링 방지, useMemo와 useCallback의 필요성, > 렌더링 최적화를 위해 필요한 React.memo를 올바르게 사용하기 위한 것들을 이해할 수 있다.
1.2.1 함수란 무엇인가?
🙋 내가 생각하는 좋은 함수란?
함수는 하나의 역할만을 해야 한다 (단일 책임 원칙)
그래서 인자와 반환값을 잘 고려해야 하고, 무슨 역할을 하는지에 따라 직관적인 네이밍을 하는 게 유지보수성을 높일 수 있다.
1.2.2 함수를 정의하는 4가지 방법
본인이나 프로젝트의 상황에 맞는 작성법을 일관되게 사용하자.
함수 선언문
function add(a, b) {
return a + b;
}
어떠한 값도 표현하지 않으므로 일반 문으로 분류
함수 호이스팅 발생해 선언 이전에 호출 가능 → 관리해야하는 스코프가 길어질수록 나쁘게 작용
이름이 있는 함수 리터럴의 경우, 코드 문맥에 따라 선언문 또는 표현식으로 해석될 수 있다.
const sum = function (a, b) {
return a + b;
};
sum(10, 24); // 34
함수 표현식
const sum = function (a, b) {
return a + b;
};
sum(10, 11); // 21
Function 생성자
const add = new Function("a", "b", "return a + b");
add(10, 11); // 21
화살표 함수
const add = (a, b) => {
return a + b;
};
const add = (a, b) => a + b;
ES6에서 새롭게 추가된 함수 생성 방식
다른 함수 생성 방식과의 차이점
생성자 함수로 사용할 수 없다. constructor를 사용할 수 없기 때문이다.
const Car = (name) => {
this.name = name;
};
const myCar = new Car("carr"); // TypeError: Car is no ta constructor
arguments가 존재하지 않는다.
function hello() {
console.log(arguments);
}
hello(1, 2); // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
const hi = () => {
console.log(arguments);
};
hi(1, 2); // Uncaught ReferenceError: arguments is not defined
⭐️ this 바인딩이 존재하지 않아 상위 스코프의 this를 따른다.
즉, 화살표 함수의 this는 상위 스코프의 this를 상속받아 혼란을 줄일 수 있다.
클래스형 컴포넌트에서 이벤트에 바인딩할 메서드 선언 시 차이가 있다.
import React from "react";
class Component extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 1,
};
}
ArrayFunctionCountUp = () => {
console.log(this); // undefined
this.setState((prev) => ({ counter: prev.counter + 1 }));
};
functionCountUp() {
console.log(this); // Class Component
this.setState((prev) => ({ counter: prev.counter + 1 }));
}
render() {
return (
<div>
<p>{this.state.counter}</p>
<button type="button" onClick={this.functionCountUp}>
일반 함수
</button>
<button type="button" onClick={this.ArrayFunctionCountUp}>
화살표 함수
</button>
</div>
);
}
}
export default Component;
🙋 함수 표현식과 함수 선언식의 차이
- 호이스팅 여부
- 함수 선언문: 함수 호이스팅 발생
- 함수 실행 전에 메모리에 등록해두어 선언 이전에 실행 가능
- 함수 표현식: 변수 호이스팅 발생
- 변수의 종류에 따른 반환값이 다르다
(var - undefined / let, const - Reference Error)
this
- 자신이 속한 객체나 자신이 생성할 인스턴스를 가리키는 값
- 함수가 어떻게 호출되느냐에 따라 동적으로 결정
1.2.3. 다양한 함수 살펴보기
즉시 실행 함수(Immediately Invoked Function Expression, IIFE)
(function (a, b) {
return a + b;
})(10, 14);
((a, b) => {
return a + b;
})(10, 14);
함수를 정의하고 즉시 실행되는 함수로, 재사용할 수 없다.
글로벌 스코프를 오염시키지 않는 독립적 함수 스코프 생성하는 장점이 있다.
다시 호출되지 않는 것이 확실하기 때문에 리팩터링에도 도움이 된다.
고차 함수(Higher Order Function)
// 함수를 매개변수로 받는 고차 함수
const doubledArray = [1, 2, 3].map((item) => item * 2);
console.log(doubledArray); // [2, 4, 6]
// 함수를 반환하는 고차 함수
const add = function (a) {
return function (b) {
return a + b;
};
};
add(1)(3); // 4
고차 컴포넌트(Higher Order Component, HOC)
고차 컴포넌트는 고차 함수처럼 컴포넌트를 인수로 받아 다른 컴포넌트를 반환한다. 이를 통해 컴포넌트 내부에서 공통으로 관리되는 로직을 분리해 관리할 수 있어 효율적으로 리팩터링할 수 있다.
사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?
// 사용자 정의 훅 function HookComponent() { const { loggedIn } = useLogin(); useEffect(() => { if (!loggedIn) { // do something... } }, [loggedIn]); } // 고차 컴포넌트 const HOCComponent = withLoginComponent(() => { // do something... });
- 두 가지 모두 중복된 로직을 분리해 별도로 관리할 수 있다. 이를 통해 컴포넌트의 크기를 줄이고 가독성을 향상시키는 데 도움을 줄 수 있다.
- 사용자 훅이 필요한 경우
- 컴포넌트 전반에 걸쳐 동일한 로직으로 값을 제공하거나 리액트 훅을 작동시키기 위해 사용
- 컴포넌트 내부에 미치는 영향을 최소화해 개발자가 훅을 원하는 방향으로만 사용할 수 있다는 장점
- ex) uselogin 커스텀 훅은 loggedIn에 대한 값만 제공하고, 이에 대한 처리는 컴포넌트를 사용하는 쪽에서 원하는 대로 사용. 부수 효과가 비교적 제한적이다.
- 고차 컴포넌트를 사용해야 하는 경우
- 함수형 컴포넌트의 반환값, 즉 렌더링에도 영향을 미치는 공통 로직인 경우
- 고차 컴포넌트가 어떤 일을 하는지, 어떤 렌더링 결과물을 반환하는지는 직접 봐야 알 수 있기 때문에 예측하기 어렵다는 단점
- ex) 로그인되지 않은 사용자가 컴포넌트에 접근하려 . 할때 로그인을 요구하는 공통 컴포넌트를 노출, 특정 에러가 발생했을 때 현재 컴포넌트 대신 에러 발생을 알리는 컴포넌트를 노출(ErrorBoundary)
함수의 부수 효과를 최대한 억제하라.
순수 함수: 부수 효과가 없는 함수. 동일한 인수를 받으면 동일한 결과를 반환하여 예측 가능하며 안정적이다.
웹 애플리케이션을 만드는 과정에서 외부에 영향을 미치는 부수 효과는 피할 수 없지만, 최소화하도록 함수를 작성해야 한다.
리액트 관점에서는 useEffect의 작동을 최소화하여 컴포넌트의 안정성을 높이도록 해야 한다.
결론적으로, 함수에서 가능한 부수 효과를 최소화하고, 함수의 실행과 결과를 최대한 예측 가능하도록 설계해야 한다. 이는 유지보수에 도움이 된다.
가능한 한 함수를 작게 만들어라.
하나의 함수에서 너무 많은 일을 하지 마라.
ESLint의 max-line-per-function 규칙은 50줄이 넘어가면 과도하게 큰 함수로 분류한다.
누구나 이해할 수 있는 이름을 붙여라.
가능한 한 함수 이름은 간결하고 이해하기 쉽게 붙이는 것이 좋다.
useEffect나 useCallback 등의 훅에 넘겨주는 콜백 함수에 이름을 붙여주면 가독성에 도움이 된다.
useEffect(function apiRequest() {
// do something..
}, []);
함수형 컴포넌트에 대한 이해는 클로저에 달려있다.
1.4.1 클로저의 정의
클로저는 함수와 함수가 선언된 렉시컬 스코프의 조합
함수형 프로그래밍의 중요한 개념인 ‘부수 효과가 없고 순수해야 한다’는 목적 달성을 위해 사용됨
렉시컬 스코프: 코드가 작성된 순간에 정적으로 결정되는 환경. 함수가 정의된 위치에 따라 함수의 상위 스코프가 결정
1.4.2 변수의 유효 범위, 스코프
스코프: 변수의 유효 범위
전역 스코프
const global = "global scope";
function hello() {
console.log(global);
}
console.log(global); // global scope
hello(); // global scope
console.log(global === window.global); // true
함수 스코프(=지역 스코프)
const x = 10;
function foo() {
const x = 100;
console.log(x); // 100
function bar() {
const x = 1000;
console.log(x); // 1000
}
bar();
}
console.log(x); // 10
foo();
1.4.3 클로저의 활용
function outerFunction() {
const x = "hello";
function innerFunction() {
console.log(x); // hello
}
return innerFunction;
}
const innerFunction = outerFunction();
innerFunction();
반환한 함수 innerFunction에는 x가 존재하지 않지만, 해당 함수가 정의된 렉시컬 스코프에는 x가 존재하여 접근할 수 있기 때문에 정상적으로 ‘hello’를 출력할 수 있다.
클로저의 활용
전역 스코프의 사용을 막고, 개발자가 원하는 정보만 노출시킬 수 있다.
// 클로저 미사용: 전역 스코프
var counter = 0;
function handleClick() {
counter += 1;
return counter;
}
// 클로저 사용:
function Counter() {
let counter = 0;
return {
increase() {
counter += 1;
return counter;
},
decrease() {
counter -= 1;
return counter;
},
counter() {
console.log("counter에 접근");
return counter;
},
};
}
const c = new Counter();
console.log(c.increase()); // 1
console.log(c.counter()); // counter에 접근 1
리액트에서의 클로저
1.4.4 주의할 점
클로저를 잘못 사용하면 예상치 못한 결과를 볼 수도 있다.
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000);
} // 5 5 5 5 5
전역 변수 i는 for문이 완료된 이후 setTimeout을 실행할 때 이미 5로 업데이트되어 있다.
이를 해결하기 위해서는 블록 단위로 스코프를 만들어줘야 한다.
블록 레벨 스코프를 갖는 let 사용하기
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000);
} // 0 1 2 3 4
즉시 실행 함수로 반복문 블록마다 스코프 생성
for (var i = 0; i < 5; i++) {
setTimeout(
(function (sec) {
return function () {
console.log(sec);
};
})(i),
i * 1000
);
} // 0 1 2 3 4
클로저 사용 시 주의 점은 선언적 환경을 기억하기 위한 비용이 발생한다는 것이다.
비동기 코드 작동 방식을 이해하면 자바스크립트가 작업을 동시에 처리하는 방법, 태스크에 대한 우선순위, 주의할 점을 파악해 더욱 매끄러운 웹 애플리케이션 서비스를 제공할 수 있다.
동기 방식과 비동기 방식
- 동기 방식은 직렬 방식으로 요청이 시작된 이후 응답을 받아야 다른 작업을 처리한다.
- 직관적이지만 한 번에 많은 작업을 처리할 수 없다.
- 비동기 방식은 병렬 방식으로 응답을 받지 않아도 다음 작업을 처리한다.
- 한 번에 여러 작업이 실행될 수 있다.
1.5.1 싱글 스레드 자바스크립트
프로세스와 스레드
자바스크립트는 왜 싱글 스레드로 설계되었을까?
최초의 자바스크립트는 브라우저에서 HTML을 그리는 제한적 용도였고, 동시에 여러 스레드에서 DOM을 조작할 경우 DOM 표시에 문제가 생길 수 있기 때문.
자바스크립트는 싱글 스레드로 코드의 실행이 하나의 스레드에서 순차적으로 이루어지지만, 자바스크립트 런타임 외부에서 이벤트 루프를 사용해 비동기 코드를 처리할 수 있다.
1.5.2 이벤트 루프란?
호출 스택과 이벤트 루프
호출 스택(=콜스택): 자바스크립트에서 수행해야 할 코드나 함수를 순차적으로 담아두는 스택
⭐️ 이벤트 루프: 이벤트 루프의 단일 스레드에서 콜 스택과 태스크 큐 내부에 대기 중인 함수가 있는지 반복적으로 확인하고, 실행 가능한 오래된 것부터 자바스크립트 엔진을 이용해 실행
태스크 큐: 비동기 함수의 콜백 함수나 이벤트 핸들러 같은 실행해야 할 태스크의 집합(Set)
1.5.3 태스크 큐와 마이크로 태스크 큐
이벤트 루프는 하나의 마이크로 태스트 큐를 가진다.
각 태스크에 들어가는 대표적인 작업은 다음과 같고, 마이크로 태스크 큐는 기존 태스크 큐보다 우선권을 갖는다.
function foo() {
console.log("foo");
}
function bar() {
console.log("bar");
}
function baz() {
console.log("baz");
}
setTimeout(foo, 0); // 2
Promise.resolve().then(bar).then(baz); // 1
// bar
// baz
// foo
태스크 큐: setTimeout, setInterval, setImmediate
마이크로 태스크 큐: process.nextTick, Promises, queueMicroTask, MutationObserver
브라우저 렌더링은 언제 실행될까? 마이크로 태스크 큐와 태스크 큐 사이에서 발생
발생 순서: 동기 → 마이크로 태스크 큐 → 렌더링 → 태스크 큐
동기 코드와 마이크로 태스크가 렌더링에 영향을 미칠 수 있음을 고려해 특정 렌더링이 자바스크립트 내무거운 작업과 연관이 있다면 어떤 식으로 분리해 사용자에게 좋은 경험을 제공해 줄지 고민해 보아야 한다.
일반적 자바스크립트와 비교해 리액트 코드는 JSX 구문 내부에서 객체를 조작하거나 객체의 얕은 동등 비교 문제를 피하기 위해 객체 분해 할당을 하는 등 독특하다.
사용자의 다양한 브라우저 환경과 최신 문법을 작성하고 싶은 개발자의 요구를 해결하기 위해 바벨을 사용해 자바스크립트의 최신 문법을 다양한 브라우저에서 일관적으로 지원할 수 있도록 트랜스파일할 수 있다.
바벨이 트랜스파일하는 방법과 코드 결과물을 이해하면 애플리케이션을 디버깅하는 데 도움이 된다.
1.6.1 구조 분해 할당
배열 또는 객체의 값을 분해해 개별 변수에 할당하는 것. 배열은 순서대로, 객체는 이름으로 꺼내온다.
배열 구조 분해 할당 (ES2015-ES6)
// useState
const [state, setState] = useState();
undefined
인 경우에 기본값 사용,
를 통해 인덱스에 대한 할당 생략 가능객체 구조 분해 할당 (ECMA 2018 - ES9)
function Componenet({ a = 10, b = 20 }) {
return a + b;
}
1.6.2 전개 구문
이터러블(배열, 객체, 문자열)을 전개해 간결하게 사용할 수 있는 구문
배열 전개 구문(ES6): 간편하게 배열 합성 가능
const arr1 = [1, 2];
const arr2 = arr1;
console.log(arr1 === arr2); // true
const arr3 = [1, 2];
const arr4 = [...arr3];
console.log(arr3 === arr4); // false
객체 전개 구문(ECMA 2018): 간편하게 객체 합성 가능
const obj1 = {
a: 1,
b: 2,
};
const obj2 = {
c: 3,
d: 4,
};
const newObj = { ...obj1, ...obj2 };
console.log(newObj); // { a: 1, b: 2, c: 3, d: 4 }
1.6.3 객체 초기자(ES2015)
객체 선언 시 객체에 넣고자 하는 키와 값을 가지고 있는 변수가 이미 존재한다면, 해당 값을 간결하게 넣어줄 수 있는 방식
```jsx
const a = 1
const b = 2
const obj = {
a,
b
}
// {a: 1, b: 2}
```
1.6.4 Array 프로토타입의 메서드: map, filter, reduce, forEach
Array.prototype.map
Array.prototype.filter
Array.prototype.reduce
Array.prototype.forEach
🙋 map()과 forEach()의 차이점
둘 다 배열의 요소마다 인자로 받은 콜백함수를 실행하여 얻은 반환값으로 치환한다는 공통점이 있는 반면에 새로운 배열을 반환하는지에 대한 여부가 다르다. map()은 새로운 배열을 반환하고, forEach()는 기존 배열을 변환한다.
1.6.5 삼항 조건 연산자