JS 표준 코드가 아닌 페이스북이 임의로 만든 새로운 문법 > 트랜스파일러를 거쳐서 JS 코드로 변환해야 한다.
✔️ React 컴포넌트는 반드시 대문자로 시작해야 한다. > HTML 태그와 사용자 정의 컴포넌트를 구분하기 위함
✔️ JSXElement, JSXAttributes, JSXChildren, JSXStrings 4가지 컴포넌트 기반 구성
element와 비슷한 역할const MyApp = () => {
return (
<div className="app"> {/* JSXElement + JSXAttributes */}
<h1>환영합니다!</h1> {/* JSXElement + JSXStrings */}
<Button {/* JSXElement */}
disabled={false} {/* JSXAttributes */}
onClick={handleClick} {/* JSXAttributes */}
>
클릭하세요 {/* JSXChildren + JSXStrings */}
</Button>
</div>
)
}
가능한 JSX 구조
// 하나의 요소로 구성된 가장 단순한 형태
const ComponentA = <A>안녕하세요.</A>
// 자식이 없어 SelfColsingTag로 닫혀 있는 형태
const ComponentB = <A />
// 옵션을 {}와 전개 연산자로 넣을 수 있다.
const ComponentC = <A {...{required: true}} />
// 속성만 넣어도 가능하다.
// true일 때 축약 가능
const ComponentD = <A required /> // A required={true} />
// 속성과 속값을 넣을 수 있다.
const ComponentE = <A required={false} />
const ComponentF = (
<A>
<B text="리액트" />
{/* 옵션의 값으로 JSXElement를 넣는 것 또한 올바른 문법이다. */}
<B optionalChildren={<>안녕하세요.</>} />
</A>
)
✔️ JSX > 자바스크립트 변환 방법
: JSXElement를 첫 번째 인수로 선언해 요소를 정의하고, 옵셔널인 JSXChildren, JSXAttributes, JSXStrings는 이후 인수로 넘겨주어 처리한다.
React.createElement(element, attributes, children) 형태`경우에 따라 다른 JSXElement를 렌더링해야 할 경우, 굳이 요소 전체를 감싸지 않더라도 처리할 수 있다. JSXElement만 다르고, JSXAttributes, JSXChildren이 완전히 동일한 상황에서 중복 코드 최소화
➡️ JSX 반환값이 결국 React.createElement로 귀결되기 때문에 이렇게 쉽게 리팩터링할 수 있다.
import {createElement, PropsWithChildren} from 'react'
// ❌ props 여부에 따라 children 요소만 달라지는 경우
// 굳이 번거롭게 전체 내용을 삼항 연산자로 처리할 필요가 없다.
function TextOrHeading({
isHeading,
children,
}: PropsWithChildren<{ isHeading: boolean }>) {
return isHeading ? (
<h1 className="text">{children}</h1> // className과 children이 중복
) : (
<span className="text">{children}</span> // className과 children이 중복
)
}
// ✅ JSX가 변환되는 특성을 활용한다면 간결 처리 가능
import {createElement} from 'react'
function TextOrHeading({
isHeading,
chidlren
}: PropsWithChildren<{isHeading: boolean}>) {
return createElement(
isHeading ? 'h1' : 'span', // 요소만 조건부로 변경
{ className: 'text' }, // 속성은 한 번만 정의
children, // 자식도 한 번만 정의
)
}
리액트의 특징_실제 DOM이 아닌 가상 DOM을 운영한다는 것
DOM : 웹페이지에 대한 인터페이스로, 브라우저가 웹페이지의 콘텐츠와 구조를 어떻게 보여줄지에 대한 정보를 담는다.
✔️ 브라우저가 웹사이트 접근 요청을 받고, 화면을 그리는 과정
1. 브라우저가 사용자가 요청한 주소를 방문해 HTML 파일을 다운로드한다.
2. 브라우저의 렌더링 엔진은 HTML을 파싱해 DOM 노드로 구성된 트리DOM을 만든다.
3. 2번 과정에서 CSS 파일을 만나면 CSS 파일도 다운로드한다.
4. 브라우저 렌더링 엔진은 CSS도 파싱해 CSS 노드로 구성된 트리CSSOM를 만든다.
5. 2번에서 만든 DOM 노드를 순회하는데, 모든 노드를 방문하는 것이 아니고 사용자 눈에 보이는 노드만 방문한다. display: none은 방문해 작업 X
6. 5번에서 눈에 보이는 노드를 대상으로 해당 노드에 대한 CSSOM 정보를 찾고, 여기서 발견한 CSS 스타일 정보를 이 노드에 적용한다.
layout, reflow : 각 노드가 브라우저 화면의 어느 좌표에 정확히 나타나야 하는지 계산하는 과정. > 이 레이아웃 과정을 거치면 반드시 페인팅 과정도 거치게 된다.페인팅 : 레이아웃 단계를 거친 노드에 색과 같은 실제 유효한 모습을 그리는 과정렌더링 이후 추가 렌더링 작업은, 하나의 페이지에서 모든 작업이 일어나는 싱글 페이지 애플리케이션SPA에서 더욱 많아진다.
이 SPA 특징 덕분에 사용자는 페이지의 깜빡임 없이 자연스러운 웹페이지 탐색이 가능하지만, 그만큼 DOM을 관리하는 과정에서 부담 비용이 커진다.
✔️ 가상 DOM : 실제 브라우저의 DOM이 아닌 리액트가 관리하는 가상의 DOM
웹페이지가 표시해야 할 DOM을 일단 메모리에 저장하고,
변경된 새로운 DOM과 이전 DOM을 메모리에서 비교하여
실제 브라우저의 DOM에는 차이점만 업데이트한다.
➡️ DOM 계산을 브라우저가 아닌 메모리에서 계산하는 과정을 한 번 거친다.
추가 예정
추가 예정
✔️ 함수 컴포넌트 vs. 클래스 컴포넌트
생명주기 메서드의 부재
함수 컴포넌트와 렌더링된 값 : 함수 컴포넌트는 렌더링된 값을 고정하고, 클래스 컴포넌트는 그렇지 못한다.
브라우저의 렌더링 : HTML과 CSS 리소스를 기반으로 웹페이지에 필요한 UI를 그리는 과정
리액트의 렌더링 : 브라우저가 렌더링에 필요한 DOM 트리를 만드는 과정
✔️ 리액트의 렌더링 프로세스
렌더링 결과물은 JSX 문법으로 구성돼 있고, 이것이 자바스크립트로 컴파일되면서 React.createElement()를 호출하는 구문으로 변환한다.
1. 리액트는 컴포넌트의 루트에서부터 아래쪽으로 내려가며 업데이트가 필요하다고 지정돼 있는 모든 컴포넌트를 찾는다.
- JSX → JavaScript 변환: 컴파일 과정
2. 각 컴포넌트의 렌더링 결과물을 수집한 후, 리책트의 새로운 트리인 가상 DOM과 비교해 실제 DOM에 반영하기 위한 모든 변경사항을 차례대로 수집힌다.
- React 렌더링: DOM 트리 만드는 과정
3. DOM에 적용해 변경된 결과물이 보이게 된다.
브라우저 렌더링: DOM 트리를 화면에 그리는 과정
✔️ 리액트의 렌더링이 발생하는 시나리오
1. 최초 렌더링
2. 리렌더링
...
상태 업데이트상태 업데이트index 값들과 렌더링된 key 값들이 같을 경우, 같은 컴포넌트로 인식 / 이때 key를 랜덤 값으로 하면 이전 값과 이후 값이 다르기 때문에 다른 컴포넌트로 인식✔️ 일반적인 렌더링 시나리오 살펴보기
A > B > C > D 컴포넌트
1. B 컴포넌트의 setState가 호출된다.
2. B 컴포넌트의 리렌더링 작업이 렌더링 큐에 들어간다.
3. 리액트는 트리 최상단에서부터 렌더링 경로를 검사한다.
4. A 컴포넌트는 리렌더링이 필요한 컴포넌트로 표시돼 있지 X으므로, 별다른 작업을 하지 X는다.
5. 하위 컴포넌트인 B 컴포넌트는 업데이트가 필요하다고 체크돼 있으므로 B를 리렌더링한다.
6. 5번 과정에서 B는 C를 반환한다.
7. C는 props가 업데이트됐다. 그러므로 업데이트가 필요한 컴포넌트로 체크돼 있고 업데이트한다.
8. 7번 과정에서 C는 D를 반환했다.
9. D는 업데이트가 필요한 컴포넌트로 체크되지 X았지만, C가 렌더링됐으므로 자식인 D도 렌더링된다.
9-1. 이때 D 컴포넌트에 memo가 추가될 경우, B 컴포넌트에서 상태값이 변경됐음에도 props가 변경되지 않으므로, 렌더링이 일어나지 X는다.
❓ 리렌더링 작업이 바로 실행되지 않아도 되는걸까? 렌더링 큐에 들어가면 바로 실행되는걸까?
대부분은 현재 실행 중인 JavaScript 함수가 끝나는 순간에 큐가 실행되다.
❓ 렌더링 큐에 들어갈 때 어떤 걸 체크하는지도 함께 저장되는걸까?
렌더링 큐에는 "어떤 컴포넌트를 리렌더링할지"만 들어가고, "어떤 걸 체크할지"는 실제 렌더링 과정에서 결정된다. (동적으로)
useMemo, useCallback 훅
고차 컴포넌트 memo
✔️ 꼭필요한 곳을 신중히 골라서 메모이제이션해야 한다는 입장
: 렌더링도 비용이지만, 메모리에 저장하는 것도 마찬가지로 비용이다.
✔️ 모조리 메모이제이션해 버리자는 입장
: 메모이제이션했을 때 더 많은 이점을 누릴 수 있다.
memo
어차피 리액트의 기본적인 알고리즘 때문에 이전 결과물은 어떻게든 저장해두고 있다. > 따라서 우리가 memo로 지불해야 하는 비용은 props에 대한 얕은 비교뿐이다.
물론 이 비용도 무시할 수 없지만, memo를 하지 않았을 때 발생하는 문제가 더 크다.
useMemo useCallback
모든 객체는 재생성되고, 결과적으로 참조는 달라지게 된다. > 이 달라진 참조에 대한 값을 useEffect와 같은 의존성 배열에 쓰이게 될 경우, 영향을 미치게 된다.
function useMath(number: number) {
const [double, setDouble] = useState(0)
const [triple, setTriple] = useState(0)
useEffect(() => {
setDouble(number * 2)
setTriple(number * 3)
}, [number])
// return {double, triple} // ❌ 매번 새로운 객체 생성
return useMemo(() => ({double, triple}), [double, triple]) // ✅
}
export default function App() {
const [counter, setCounter] = useState(0)
const value = useMath(10)
useEffect(() => {
console.log(value.double, value.trple)
}, [value]) // ❗️ value 객체의 참조가 매번 달라짐!
// 값이 실제로 변한 건 없는데 계속해서 출력된다.
function handleClick() {
setCounter((prev) => prev + 1)
}
return (
<>
<h1>{counter}</h1>
<button onClick={handleClick}>+</button>
// 🙆🏻♀️ JSX에서는 값 자체만 사용할 경우 (이전 렌더링 결과 === 새 렌더링 결과)
// 객체 참조가 바뀌어도 값(20, 30)은 동일하므로
// 실제 DOM에는 변화 없음
</>
)
}