-
모던 리액트 딥 다이브concept/React, Redux, RN 2024. 3. 3. 22:25
작년 11월에 모던 리액트 딥 다이브 책이 출시 되고, 바로 스터디를 시작했다. 일주일에 한 장씩 읽고 만나서 정리한 부분들 이야기 하는 시간을 가졌는데 책이 900페이지이다보니 은근히 빡셌지만 끝나니까 아주 후련했던 스터디..
그 때 정리했던 내용들 중에서 알짜배기만 골라서 메모를 하기 위한 포스팅
1장. 리액트 개발을 위해 꼭 알아야 할 자바스크립트
1.1 자바스크립트의 동등 비교
1.1.1 자바스크립트의 데이터 타입
- 원시 타입
- boolean
- null
- undefined
- number
- string
- symbol
- 중복되지 않는 어떤 고유한 값을 나타내기 위해 만들어진 타입. Symbol() 을 사용해서 제작 가능.
- ex. const key = Symbol('key')
- bigint
- number가 다룰 수 있는 숫자 크기의 제한을 극복하기 위해 ES2020에서 새롭게 나온 타입. 2^53 - 1 보다 더 큰 숫자를 저장 가능
- 객체 타입
- object
1.1.2 값을 저장하는 방식의 차이
- 원시 타입은 불변 형태의 값으로 저장
- 객체 타입은 변경 가능한 형태로 저장, 값을 복사할 때도 값이 아닌 참조를 전달
1.1.3 자바스크립트의 또 다른 비교 공식, Object.is
- 비교할 수 있는 방법: 1. == 2. === 3. Object.is
- == 는 강제로 타입 변환을 시켜 느슨한 비교 연산자로서 작동
- === 는 타입이 다른 경우에는 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 리액트에서의 동등 비교
- 리액트에서는 이 Object.is를 기반으로 동등 비교를 하는 shallowEqual 함수를 만들어 사용
- shallowEqual
- Object.is 로 먼저 비교를 수행한 다음에 객체 간 얕은 비교(첫 번째 깊이에 존재하는 값만 비교)를 한 번 더 수행
- 그 이유는 props 가 객체이고 이 props 만 일차적으로 비교하면 되기 때문
- shallowEqual
1.2 함수
1.2.1 함수란 무엇인가?
- 함수란 작업을 수행하거나 값을 계산하는 등의 과정을 표현하고, 이를 하나의 블록으로 감싸서 실행 단위로 만들어 놓은 것
1.2.2 함수를 정의하는 4가지 방법
- 함수 선언문
- 가장 일반적인 방식
- 호이스팅이 가능 → 코드의 순서에 상관없이 함수를 호출할 수 있다.
- 함수 표현식
- 함수는 ‘일급 객체’
- 함수는 다른 함수의 매개변수가 될 수도 있고, 반환값이 될 수도 있으며, 할당도 가능
- 함수를 변수에 할당하는 것은 당연히 가능
- 호이스팅은 가능하지만 런타임 시점에 함수가 할당되어 작동
- Function 생성자
- 잘 사용하지 않는 방법
- 화살표 함수
- ES6에서 새로 추가된 방식이며 가독성과 코드 글자 수가 줄어들어 많이 이용하는 방식
- 기존 함수와 차이점
- constructor 사용 불가
- arguments 없음
- this 바인딩 차이: 화살표 함수는 함수 자체의 바인딩을 갖지 않는다.
1.2.3 다양한 함수 살펴보기
- 즉시 실행 함수 (IIFE: Immediately Invoked Function Expression)
- 함수를 정의하고 그 순간 즉시 실행되는 함수, 단 한 번만 호출되고 다시금 호출할 수 없다.
- 고차 함수
- 함수를 인수로 받거나 결과로 새로운 함수를 반환하는 함수
- 이 특징을 활용해 고차 컴포넌트(Higher Order Component) 를 만들 수 있다.
1.2.4 함수를 만들 때 주의해야 할 사항
- 함수의 부수 효과(side effect)를 최대한 억제하라
- 가능한 함수를 작게 만들어라
- 누구나 이해할 수 있는 이름을 붙여라
- useEffect나 useCallback을 사용할 때 넘겨주는 콜백 함수에 네이밍을 붙여주면 가독성에 도움이 된다.
- useEffect(function apiRequest() { // do something }, [])
1.3 클래스
1.3.1 클래스란 무엇인가?
- 특정한 형태의 객체를 반복적으로 만들기 위해 사용되는 것
- constructor
- 객체를 생성하는데 사용하는 특수한 메서드, 단 하나만 존재할 수 있으며 여러 개를 사용한다면 에러가 발생
- 생성자에서 별 다르게 수행할 작업이 없다면 생략도 가능
- 프로퍼티
- 클래스로 인스턴스를 생성할 때 내부에 정의할 수 있는 속성값
- getter와 setter
- getter: 클래스에서 무언가 값을 가져올 때 사용
- setter: 클래스 필드에 값을 할당할 때 사용
- 인스턴드 메서드
- 클래스 내부에 선언한 메서드
- a.k.a. prototype 메서드
- 정적 메서드
- 클래스의 인스턴스가 아닌 이름으로 호출할 수 있는 메서드 static
- this는 사용할 수 없다.
- 전역 유틸 함수를 정적 메서드로 많이 활용
- 상속
- extends 키워드를 활용하면 기본 클래스를 기반으로 다양하게 파생된 클래스를 만들 수 있다.
1.3.2 클래스와 함수의 관계
- 클래스 작동을 생성자 함수로 유사하게 재현 가능
1.4 클로저
1.4.1 클로저의 정의
- 클로저는 함수와 함수가 선언된 어휘적 환경의 조합
1.4.2 변수의 유효 범위, 스코프
- 전역 스코프
- 전역 레벨에 선언한 스코프
- 변수를 선언하면 어디서든 호출할 수 있음.
- 함수 스코프
- 자바스크립트는 기본적으로 함수 레벨 스코프
1.4.3 클로저의 활용
- 전역 스코프의 활용을 막고, 개발자가 원하는 정보만 개발자가 원하는 방향으로 노출시킬 수 있다.
- 리액트에서의 클로저
- useState 함수의 호출이 종료되어도 setState는 useState 내부의 최신 값을 계속해서 확인이 가능
1.4.4 주의할 점
- 꼭 필요한 작업만 남겨 놓고 기억할 수 있도록 구성해야 함
- 그렇지 않으면 메모리를 불필요하게 잡아먹고 성능에 악영향을 미칠 수 있다.
1.5 이벤트 루프와 비동기 통신의 이해
- 자바스크립트는 싱글 스레드에서 작동하기 때문에 한 번에 하나의 작업만 동기 방식으로 처리할 수 있는 것이 기본이다.
- 그렇지만 이벤트 루프 등의 개념을 활용해 비동기 요청을 처리할 수 있다.
1.5.1 싱글 스레드 자바스크립트
- 프로세스
- 프로그램을 구동해 프로그램의 상태가 메모리상에서 실행되는 작업 단위, 즉 하나의 프로그램 실행은 하나의 프로세스를 가지고 그 프로세스 내부에서 모든 작업이 처리되는 것
→ 동시에 여러 개의 복잡한 작업을 수행하기 위해서 스레드라는 더 작은 실행 단위가 등장
- 스레드
- 하나의 프로세스에서 여러 개의 스레드를 만들 수 있고 스레드 끼리는 메모리를 공유할 수 있어 여러 가지 작업을 동시에 수행할 수 있다.
→ 결국 프로세스 내부에서 여러 개의 스레드를 활용하면서 동시 다발적인 작업 처리가 가능해 짐.
- 싱글 스레드 자바스크립트라는 것은 자바스크립트 코드의 실행이 하나의 스레드에서 순차적으로 이루어진다는 의미
1.5.2 이벤트 루프란?
- 이벤트 루프란 자바스크립트 런타임 외부에서 자바스크립트 비동기 실행을 돕기 위해 만들어진 장치
- 호출 스택과 이벤트 루프
- 호출 스택은 자바스크립트에서 수행해야 할 코드나 함수를 순차적으로 담아두는 스택
- 호출 스택에 실행 중인 코드가 있는지, 그리고 태스크 큐에 대기 중인 함수가 있는지 반복해서 확인하는 역할
- 호출 스택이 비었다면 태스크 큐에 대기 중인 작업이 있는지 확인하고 이 작업을 실행 가능한 오래된 것부터 순차적으로 꺼내와서 실행한다.
1.5.3 태스크 큐와 마이크로 태스크 큐
- 마이크로 태스크
- Promise
- 마이크로 태스크 큐는 기존 태스크 큐보다 우선권을 갖는다. 즉, setTimeout 과 setInterval 은 Promise 보다 늦게 실행
- 마이크로 태스크 큐 작업이 끝날 때 마다 렌더링이 실행된다.
1.6 리액트에서 자주 사용하는 자바스크립트 문법
1.6.1 구조 분해 할당
- 배열 또는 객체의 값을 분해해 개별 변수에 즉시 할당
- 배열은 이름을 변경할 수 있고, 객체는 객체 내부 이름으로 사용 가능
- useState 는 배열 구조 분해 할당을 활용한 예시
1.6.2 전개 구문
- spread syntax
- 객체, 문자열과 같이 순회할 수 있는 값에 대해 전개해서 간결하게 사용할 수 있는 구문
- 배열과 객체의 전개 구문을 활용하면 push() , concat() , splice() 등의 메서드를 사용하지 않고도 합성이 가능하다.
- 단, 객체 전개 구문에 있어서는 순서가 중요하다.
1.6.3 객체 초기자
- 객체를 선언할 때 객체에 넣고자 하는 키와 값을 가지고 있는 변수가 이미 존재한다면 해당 값을 간결하게 넣어줄 수 있는 방식
1.6.4 Array 프로토타입의 메서드: map, filter, reduce, forEach
- map: 인수로 전달받은 배열과 똑같은 길이의 새로운 배열을 반환하는 메서드
- filter: 콜백 함수를 인수로 받고, 콜백 함수에서 truthy 조건을 만족하는 경우에만 해당 원소를 반환
- reduce: 콜백 함수와 함께 초깃값을 추가로 인수를 받는데 이 초깃값에 따라 배열이나 객체, 또는 그 외의 다른 무언가를 반환할 수 있는 메서드
- forEach: 콜백 함수를 받아 배열을 순회하면서 단순히 그 콜백 함수를 실행하기만 하는 메서드
1.6.5 삼항 조건 연산자
- 자바스크립트에서 유일하게 3개의 피연산자를 취할 수 있는 문법
조건문 ? 참일 때 값 : 거짓일 때 값
- 삼항 조건 연산자는 가급적이면 가독성을 위해서 중첩하지 않는 편이 좋다.
1.7 선택이 아닌 필수, 타입스크립트
1.7.1 타입스크립트란?
- TypeScript is JavaScript with syntax for types
- 자바스크립트는 기본적으로 동적 타입의 언어이기 때문에 대부분의 에러를 코드를 실행했을 때만 확인할 수 있다는 문제점이 있다.
- 자바스크립트의 한계를 벗어나 타입 체크를 정적으로 런타임이 아닌 빌드 타임에 수행할 수 있게 해 준다.
1.7.2 리액트 코드를 효과적으로 작성하기 위한 타입스크립트 활용법
- any 대신 unknown을 사용하자
- any는 타입스크립트를 사용하는 의미가 없어지게 만들기 때문에 정말 예외적인 경우에만 사용하는 것이 좋다.
- 대신 아직 타입을 단정할 수 없는 경우에는 unknown 을 사용하는 것이 좋다.이렇게 typeof를 사용해서 unknown에 직접 접근하는 대신 해당 unknown 값이 우리가 원하는 타입일 때만 의도대로 작동하도록 코드를 작성
- function doSomething(callback: unknown) { if (typeof callback === 'function') { callback() } throw new Error('callback은 함수여야 합니다.') }
- unknown: top type VS never: bottom type
- never은 어떤 타입도 들어올 수 없음을 의미, 코드상으로 존재가 불가능한 타입
- 타입 가드를 적극 활용하자
- 타입을 사용할 때는 타입을 좁히는 것이 좋다. 타입을 좁히는데 도움을 주는 것이 타입 가드
- instanceof
- 클래스로 타입을 만들어서 타입 체크를 할 때 해당 클래스의 인스턴스인지 체크
- typeof
- 특정 요소에 대한 자료형을 확인
- in
- 어떤 객체에 키가 존재하는지 확인하는 용도
- 제네릭
- 함수나 클래스 내부에서 단일 타입이 아닌 다양한 타입에 대응할 수 있도록 도와주는 도구 <>
const [state, setState] = useState<string>('')
- 인덱스 시그니처
- 객체의 키를 정의하는 방식, 키에 원하는 타입을 부여할 수 있다.
여기서 [key: string] 이 부분이 인덱스 시그니처type Hello = { [key: string]: string }
- 인덱스 시그니처를 사용할 경우 key의 범위가 string, number 등 커지기 때문에 키의 타입을 좁히는 방법이 필요하다.
- Record<Key, Value> 혹은 type 을 활용해 타입을 좁힐 수 있다.
- Object.keys 를 활용해서 객체의 키들을 뽑아낼 경우 타입 에러가 발생할 수 있다.
- 이럴 때는 as 를 활용해 타입 단언을 해 줄 수 있다.
1.7.3 타입스크립트 전환 가이드
- tsconfig.json 먼저 작성하기
- 주요 옵션 목록
- outDir : .ts 나 .js 가 만들어진 결과를 넣어두는 폴더
- allowJs : .js 파일을 허용할 것인지
- target : 결과물이 될 자바스크립트 버전 지정
- include : 트랜스파일 할 자바스크립트와 타입스크립트 파일 지정
- 주요 옵션 목록
- JSDoc과 @ts-check를 활용해 점진적으로 전환하기
- 자바스크립트 파일 최상단에 //@ts-check를 선언하고 JSDoc을 활용해 변수나 함수에 타입을 제공하면 타입 체크 가능
- 타입 기반 라이브러리 사용을 위해 @types 모듈 설치하기
- 자바스크립트 기반으로 작성된 라이브러리를 타입스크립트에서 사용하기 위해서는 @types 모듈을 설치
- ex. @types/react
- 파일 단위로 조금씩 전환하기
알게 된 부분
- 클로저라는 개념이 추상적으로만 느껴졌었는데 useState가 클로저를 활용한 개념이라는 것을 알고나니 훨씬 개념 잡기 명확해진 느낌!
- 태스크 큐와 마이크로 태스크 큐가 나눠진다는 것도 처음 알게 되었다. Promise가 setTimeout, setInterval 보다 더욱 우선적으로 실행!
- 타입스크립트 꿀팁들이 유용했다. as 로 타입 단언하는 것을 지양한다고 알고 있었는데 Object.keys 로 인해 타입이 넓게 설정될 경우 as가 필요하다는 것을 알게 됨!
- 그래서 타입 단언을 사용하는 경우를 조금 더 찾아보았다.
- HTML 요소에 접근하게 될 경우
- document.getElementById("root") 를 변수에 할당하게 되면 해당 요소는 정확하게 HTMLDivElement 밖에 없다는 것을 알기 때문에 단언 사용을 해도 무방!
- stringify 된 JSON 값을 parsing 해서 사용하는 경우
- JSON.parse를 거치고 난 값은 any 타입으로 나오기 때문에 파싱한 후 타입을 갖게 하기 위해서 as를 활용해 단언 사용이 가능!
2장. 리액트 핵심 요소 깊게 살펴보기
2.1 JSX 란?
- JSX는 리액트에 종속된 문법은 아니다.
- 자바스크립트 표준 문법도 아니며 페이스북이 임의로 만든 새로운 문법이기 때문에 트랜스파일러를 거쳐야 비로소 자바스크립트 런타임이 이해할 수 있는 의미있는 자바스크립트 코드로 변환
2.1.1 JSX의 정의
- JSXElement
- JSXAttributes
- JSXChildren
- JSXStrings
2.1.2 JSX 예제
2.1.3 JSX는 어떻게 자바스크립트에서 변환될까?
- @babel/plugin-transform-react-jsx 플러그인이 JSX 구문을 자바스크립트가 이해할 수 있는 형태로 변환한다.
- 반환값은 결국 React.createElement로 귀결된다.
2.1.4 정리
- JSX는 자바스크립트 코드 내부에 HTML과 같은 트리 구조를 가진 컴포넌트를 표현할 수 있다는 점에서 각광받고 있다.
2.2 가상 DOM과 리액트 파이버
2.2.1 DOM과 브라우저 렌더링 과정
- DOM이란 웹페이지에 대한 인터페이스로 브라우저가 웹페이지의 콘텐츠와 구조를 어떻게 보여줄지에 대한 정보를 담고 있다.
- 브라우저의 렌더링 과정
- 브라우저가 사용자가 요청한 주소를 방문해 HTML 파일을 다운로드 한다.
- 브라우저의 렌더링 엔진은 HTML을 파싱해 DOM 노드로 구성된 트리 (DOM)을 만든다.
- 2번 과정에서 CSS 파일을 만나면 해당 CSS 파일도 다운로드한다.
- 브라우저의 렌더링 엔진은 이 CSS도 파싱해 CSS 노드로 구성된 트리(CSSOM)를 만든다.
- 브라우저는 2번에서 만든 DOM 노드를 순회하는데, 여기서 모든 노드를 방문하는 것이 아니고 사용자 눈에 보이는 노드만 방문한다.
- 5번에서 제외된 눈에 보이는 노드를 대상으로 해당 노드에 대한 CSSOM 정보를 찾고 여기서 발견한 CSS 스타일 정보를 이 노드에 적용한다.
- 레이아웃(layout, reflow): 각 노드가 브라우저 화면의 어느 좌표에 정확히 나타나야 하는지 계산하는 과정, 이 레이아웃 과정을 거치면 반드시 페인팅 과정도 거치게 된다.
- 페인팅(painting): 레이아웃 단계를 거친 노드에 색과 같은 실제 유효한 모습을 그리는 과정
2.2.2 가상 DOM의 탄생 배경
- 렌더링 과정은 매우 복잡하고 많은 비용이 든다. 여기서 사용자의 인터랙션이 더해지면 훨씬 복잡한 작업을 수행해야 한다.
- 특히 Single Page Application 에서는 페이지가 변경되는 경우 하나의 페이지에서 계속해서 요소의 위치를 재계산하기 때문에 그만큼 DOM을 관리하는 과정에서 부담해야 할 비용이 커진다.
- 이 문제를 해결하기 위해 탄생한 것이 바로 가상 DOM이다.
- 가상 DOM은 웹페이지가 표시해야 할 DOM을 일단 메모리에 저장하고 리액트가 실제 변경에 대한 준비가 완료되었을 때 실제 브라우저의 DOM에 반영한다.
- 가상 DOM은 브라우저에서 변경이 일어나는 것이 아니라 메모리에서 계산하는 과정을 거치는 것이기 때문에 브라우저와 개발자의 부담을 덜 수 있다.
2.2.3 가상 DOM을 위한 아키텍처, 리액트 파이버
- 리액트 파이버는 가상 DOM과 실제 DOM을 비교해 변경 사항을 수집하며 만약 이 둘 사이에 차이가 있으면 변경에 관련된 정보를 가지고 있는 파이버를 기준으로 화면에 렌더링을 요청하는 역할을 한다.
- 비동기로 이루어 진다.
2.3 클래스형 컴포넌트와 함수형 컴포넌트
2.3.1 클래스형 컴포넌트
- constructor()
- props
- state
- 메서드
- 클래스형 컴포넌트의 생명주기 메서드
- 마운트: 컴포넌트가 생성되는 시점
- 업데이트: 이미 생성된 컴포넌트의 내용이 변경(업데이트)되는 시점
- 언마운트: 컴포넌트가 더 이상 존재하지 않는 시점
- render(): 마운트와 업데이트 과정에서 일어난다. 이 함수는 항상 순수해야 하며 부수 효과가 없어야 한다.
- componentDidMount(): setState로 state 변경이 가능하지만 생성자에서 할 수 없는 것, API 호출 후 업데이터, DOM에 의존적인 작업(이벤트 리스너 추가) 등을 하는 작업 이외에 남용하지 말 것.
- componentDidUpdate(): setState로 state 변경이 가능하지만 적절한 조건문을 통해 성능 이슈가 발생하지 않도록 할 것.
- componentWillUnMount(): setState를 호출할 수 없다. API 호출을 취소하거나 setInterval, setTimeout 타이머를 지우는 작업에 유용
- shouldComponentUpdate(): state나 Props의 변경으로 리렌더링 되는 것을 막고 싶다면 이 메서드를 사용하면 된다.
- static getDerivedStateFromProps(): render()를 호출하기 직전에 호출되며 여기서 반환하는 객체는 state로 들어간다.
- getSnapShotBeforeUpdate(): DOM이 업데이트되기 직전에 호출되어 componentDidUpdate로 반환값이 전달된다. DOM에 렌더링 되기 전에 윈도우 크기를 조절하거나 스크롤 위치를 조정하는 작업에 유용
- getDerivedStateFromError(): 자식 컴포넌트에서 에러가 발생했을 때 호출되는 에러 메서드.
- componentDidCatch(): 자식 컴포넌트에서 에러가 발생했을 때 실행, getDerivedStateFromError에서 에러를 잡고 state를 결정한 이후에 실행
- 클래스형 컴포넌트의 한계
- 데이터 흐름을 추적하기 어렵다. 서로 다른 여러 메서드에서 state의 업데이트가 일어날 수 있기 때문
- 애플리케이션 내부 로직의 재사용이 어렵다.
- 기능이 많아질수록 컴포넌트의 크기가 커진다.
- 클래스는 함수에 비해 상대적으로 어렵다.
- 코드 최종 번들 크기를 최적화하기 어렵다.
- 핫 리로딩(애플리케이션을 실행한 채로 코드의 수정 내용이 바로바로 반영되는 것)을 하는데 상대적으로 불리하다.
2.3.2 함수형 컴포넌트
2.3.3 함수형 컴포넌트 vs 클래스형 컴포넌트
- 생명주기 메서드의 부재
- 함수형 컴포넌트는 useEffect 를 활용해서 생명주기 메서드를 비슷하게 구현할 수 있다.
- 함수형 컴포넌트와 렌더링된 값
- 함수형 컴포넌트는 렌더링이 일어날 때 마다 그 순간의 값인 props와 state를 기준으로 렌더링된다. props와 state가 변경된다면 다시 한번 그 값을 기준으로 함수가 호출된다. 반면 클래스형 컴포넌트는 시간의 흐름에 따라 변화하는 this를 기준으로 렌더링이 일어난다.
2.4 렌더링은 어떻게 일어나는가?
2.4.1 리액트의 렌더링이란?
- 리액트 애플리케이션 트리 안에 있는 모든 컴포넌트들이 현재 자신들이 가지고 있는 props와 state의 값을 기반으로 어떻게 UI를 구성하고 이를 바탕으로 어떤 DOM 결과를 브라우저에 제공할 것인지 계산하는 일련의 과정
2.4.2 리액트의 렌더링이 일어나는 이유
- 최초 렌더링
- 리렌더링
- class component - setState가 실행되는 경우
- class component - forceUpdate가 실행되는 경우
- function component - useState()의 두번째 배열 요소인 setter가 실행되는 경우
- function component - useReducer()의 두번째 배열 요소인 dispatch가 실행되는 경우
- 컴포넌트의 key props가 변경되는 경우
- props가 변경되는 경우
- 부모 컴포넌트가 렌더링될 경우 자식 컴포넌트도 무조건 리렌더링 된다.
2.4.3 리액트의 렌더링 프로세스
- 재조정 (Reconciliation)
2.4.4 렌더와 커밋
- 렌더 단계는 컴포넌트를 렌더링하고 변경 사항을 계산하는 모든 작업 (type props key 중 변경 사항을 가상 DOM과 비교해 변경이 필요한 컴포넌트를 체크하는 단계)
- 커밋 단계는 렌더 단계의 변경 사항을 실제 DOM에 적용해 사용자에게 보여주는 과정
2.4.5 일반적인 렌더링 시나리오 살펴보기
2.5 컴포넌트와 함수의 무거운 연산을 기억해 두는 메모이제이션
- useMemo
- useCallback
- memo
2.5.1 주장 1: 섣부른 최적화는 독이다. 꼭 필요한 곳에만 메모이제이션을 추가하자.
- 렌더링도 비용이지만 메모리에 저장하는 것도 마찬가지로 비용이다.
- 메모이제이션으로 인한 성능 개선이 렌더링보다 낫지 않다면 결국 안하느니만 못하는 상황이다.
2.5.2 주장 2: 렌더링 과정의 비용은 비싸다. 모조리 메모이제이션 해 버리자.
- memo 작업은 리액트의 재조정 알고리즘과 비슷해 이미 저장되고 있다.
- 결론적으로는 메모이제이션은 하는 것이 더 좋다.
- 비록 섣부른 초기화라고 할지라도 했을 때 누릴 수 있는 이점, 이를 실수로 빠뜨렸을 때 치러야할 위험 비용이 더 크기 때문에 가능한 모든 곳에 메모이제이션을 하자.
3장. 리액트 훅 깊게 살펴보기
3.1 리액트의 모든 훅 파헤치기
3.1.1 useState
- 함수형 컴포넌트 내부에서 상태를 정의하고, 이 상태를 관리할 수 있게 해 주는 훅
- 193p 코드 참고
- useState의 내부 구조는 클로저를 활용해서 state의 값을 유지하고 사용한다.
- 게으른 초기화
- useState의 초깃값에 변수 대신 함수를 넘기는 것
- 이 방법은 초깃값이 복잡하거나 무거운 연산을 포함하고 있을 때 사용할 것
- 예시) localStorage, sessionStorage에 접근 / map, filter, find 같은 배열에 대한 접근 등
- 여기서 넘겨지는 함수는 오로지 state가 처음 만들어질 때만 사용되고 이후에 리렌더링이 될 때는 무시되기 때문
3.1.2 useEffect
- 클린업 함수
- useEffect 내에서 반환되는 함수
- 이전 state를 참조해 실행된다. = 말 그대로 이전 상태를 청소해 주는 개념
- useEffect에 이벤트를 추가하면 클린업 함수에서 삭제가 필요함 (무한 이벤트 추가를 방지하기 위해서)
- 의존성 배열
- 빈 배열 - 최초 렌더링 시에만 수행
- undefined - 매 렌더링 시에 수행
- useEffect 효과
- 매 렌더링시 마다 수행되는 것은 useEffect를 쓰나 안쓰나 동일하지만 클라이언트 사이드에서 실행되는 것을 보장해주는 효과가 있다.
- useEffect를 쓰지 않으면 컴포넌트에 렌더링되는 도중에 실행되지만, useEffect를 사용하면 컴포넌트의 렌더링이 완료된 이후에 실행된다. → 즉, 렌더링을 방해해 성능에 악영향을 미칠 수 있다.
3.1.7 useReducer
- useState의 심화 버전
- 반환값은 useState와 동일하게 길이가 2인 배열
- 인수는 2~3개를 필요로 함
- reducer: useReducer의 첫번째 action을 정의
- initialState: useReducer의 초깃값
- init: useState의 인수로 함수를 넘겨줄때 처럼 초깃값을 지연해서 생성시키고 싶을 때 사용하는 함수 (게으른 초기화)
- 목적: state 값을 변경하는 시나리오를 제한적으로 두고 이에 대한 변경을 빠르게 확인할 수 있게끔 하는 것
3.1.8 useImperativeHandle
- forwardRef
- ref를 상위 컴포넌트에서 하위 컴포넌트로 전달하고 싶은 경우? ref라는 변수를 그대로 prop으로 넘기면 에러가 뜬다. 이름을 바꾸면 에러는 뜨지 않음. 그런데 forwardRef를 사용하면 ref라는 변수로 prop을 넘겨도 에러가 뜨지 않는다.
- useImperativeHandler
- 부모에게서 넘겨받은 ref를 원하는대로 수정할 수 있는 훅
- alert 등 추가적인 동작을 정의할 수 있다.
3.1.9 useLayoutEffect
- 이 함수의 시그니처는 useEffect와 동일하나 모든 DOM의 변경 후에 동기적으로 발생
- 리액트가 DOM을 업데이트
- useLayoutEffect 를 실행
- 브라우저에 변경 사항을 반영
- useEffect 를 실행
- DOM은 계산됐지만 이것이 화면에 반영되기 전에 하고 싶은 작업이 있을 때 활용
- 특정 요소에 따라 DOM 요소를 기반으로 한 애니메이션, 스크롤 위치를 제어하는 등
3.1.10 useDebugValue
- 개발 단계에서 사용하는 훅 (프로덕션 X)
- 디버깅하고 싶은 정보를 이 훅에 사용하면 리액트 개발자 도구에서 볼 수 있다.
- 사용자 정의 훅 내부의 내용에 대한 정보를 남길 수 있는 훅, 두 번째 인수로 함수를 전달하면 인수의 값이 변경됐을 때 호출되어 값을 노출
3.1.11 훅의 규칙
- react-hook/rules-of-hooks
- 최상위에서만 훅을 호출해야 한다.
- 반복문이나 조건문, 중첩된 함수 내에서 훅을 실행할 수 없다.
→ 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 훅이 호출되는 것을 보장할 수 있다.
- 함수형 컴포넌트, 사용자 정의 훅 내부에서만 훅을 호출할 수 있다.
- 그래서 훅은 항상 실행 순서를 보장받을 수 있는 컴포넌트 최상단에 선언되어야 한다.
3.2 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?
3.2.1 사용자 정의 훅
- 고차 컴포넌트은 어디서나 사용 가능
- 사용자 정의 훅은 리액트에서만 사용 가능
- 규칙: 반드시 use로 시작하는 함수를 만들어야 한다.
3.2.2 고차 컴포넌트
- 컴포넌트 자체의 로직을 재사용하기 위한 방법
- React.memo
- 대표적인 고차 컴포넌트 API
- 렌더링하기 앞서 props를 비교해 이전과 props가 같다면 렌더링 자체를 생략하고 이전에 기억해 둔 memoization 컴포넌트를 반환
- 컴포넌트에 memo()로 감싸는 방식으로 사용
- 고차 함수 만들어보기
- 고차 함수를 활용하면 함수를 인수로 받거나 새로운 함수를 반환해 완전히 새로운 결과를 만들어낼 수 있다.
- 고차 컴포넌트 만들어 보기
- with 로 시작해야 한다.
- 부수 효과를 최소화
- props를 임의로 수정, 추가, 삭제 하면 안된다.
- 계속 컴포넌트를 감쌀 경우 복잡성이 커진다는 단점이 있다.
- 최소한으로 사용하는 것이 좋다.
3.2.3 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?
- 사용자 정의 훅이 필요한 경우
- 훅으로만 공통 로직을 격리할 수 있다면 사용자 정의 훅을 사용하는 것이 좋다.
4장. 서버 사이드 렌더링
4.1 서버 사이드 렌더링이란?
4.1.1 싱글 페이지 애플리케이션의 세상
- 싱글 페이지 애플리케이션이란?
- 렌더링과 라우팅에 필요한 대부분의 기능을 서버가 아닌 브라우저의 자바스크립트에 의존하는 방식
- 최초에 첫 페이지에서 모든 데이터를 불러오고 이후 페이지 전환을 위한 모든 작업은 자바스크립트와 브라우저의 history.pushState history.replaceState 로 이루어 진다.
- 실제로 크롬 개발자 도구의 소스 보기를 들어가보면 <body> 태그 내부에 아무 내용이 없다. 자바스크립트 코드로 렌더링하기 때문이다.
- 장점: 초기 로딩 속도가 느리고 초기 로드하는 데이터 양이 많다.
- 단점: 초기 로딩 이후에는 서버를 거치지 않아도 되기 때문에 사용자에게 UI/UX 측면에서 훌륭하다.
- 싱글 페이지 렌더링 방식의 유행과 JAM 스택의 등장
- PHP, JSP 기반 웹 앱: 서버 사이드 렌더링
- CommonJS, AMD의 등장으로 자바스크립트가 서서히 다양한 작업을 수행
- Backbone.js, AngularJS, Knockout.js의 등장
- React, Vue, Angular의 등장으로 싱글 페이지 애플리케이션의 인기가 시작
- 이 인기로 인해 JAM 스택이 등장
- JavaScript | API | Markup
- 이후 MEAN 스택이나 MERN 스택처럼 API 서버도 자바스크립트로 구현하는 구조가 인기
- MongoDB | Express.js | AngularJS | Node.js
- MongoDB | Express.js | React | Node.js
4.1.2 서버 사이드 렌더링이란?
- 도입 배경
- 앞서 싱글 페이지 애플리케이션의 단점이 자바스크립트가 처길 수록 점점 웹페이지가 느려질 수 밖에 없다는 것이다.
- 이 상황에 대한 문제 의식을 한계로 두고 이를 개선하고자 서버 사이드 렌더링 방식이 다시 떠오르고 있다.
- CSR: 자바스크립트 번들에서 렌더링을 담당
- SSR: 서버에서 렌더링 담당
- CSR은 사용자 기기의 성능에 영향을 받지만 SSR은 서버에서 제공하기 때문에 비교적 안정적인 렌더링이 가능
- 서버 사이드 렌더링의 장점
- 최초 페이지 진입이 비교적 빠르다.
- 검색 엔진과 SNS 공유 등 메타데이터 제공이 쉽다.
- 로봇이 페이지에 진입
- 로봇이 html을 다운로드 (자바스크립트 코드는 실행하지 않음)
- 오픈 그래프나 메타 태그 정보를 기반으로 페이지의 검색 정보를 가져오고 검색 엔진에 저장
- 누적 레이아웃 이동이 적다.
- 누적 레이아웃 이동: 사용자에게 페이지를 보여준 이후에 뒤늦게 어떤 HTML 정보가 추가되거나 삭제되어 마치 화면이 덜컥거리는 것 같은 부정적 사용자 경험
- 서버 사이드 렌더링은 요청이 완전히 완료된 이후에 완성된 페이지를 제공하기 때문에 이런 문제가 적다.
- 사용자의 디바이스 성능에 비교적 자유롭다.
- 보안에 좀 더 안전하다.
- 애플리케이션의 모든 활동이 브라우저에 노출되지 않고 인증 또는 민감한 작업을 서버에서 수행하고 그 결과만 브라우저에 제공
- 서버 사이드 렌더링의 단점
- 소스코드를 작성할 때 항상 서버를 고려해야 한다.
- 적절한 서버가 구축되어 있어야 한다.
- 서비스 지연에 따른 문제
- 최초 렌더링에 지연이 발생된다면 서버에서 사용자에게 보여줄 페이지에 대한 렌더링 작업이 끝나기 까지 사용자에게 그 어떤 정보도 제공할 수 없다 → dynamic import 나 Suspense 같은 것들로 개선은 가능
4.1.3 SPA와 SSR을 모두 알아야 하는 이유
- 서버 사이드 렌더링 역시 만능이 아니다.
- 관리 포인트만 클라이언트에서 클라이언트 + 서버로 늘어나게 하는 역효과가 될 수 있다.
- 싱글 페이지 애플리케이션과 서버 사이드 렌더링 애플리케이션
- 멀티 페이지 애플리케이션 : 서버 사이드 렌더링
- 멀티 페이지 애플리케이션에서 발생하는 라우팅으로 인한 문제를 해결하기 위한 다양한 API (아래 기법들을 SPA에서 구현하기 위해서는 복잡한 과정을 거쳐야 하지만 멀티 페이지 애플리케이션에서는 API를 통해 제공이 됨)
- 페인트 홀딩: 같은 출처에서 라우팅이 일어날 경우 화면을 잠깐 하얗게 띄우는 대신 이전 페이지의 모습을 잠깐 보여주는 기법
- back forward cache: 브라우저 앞으로 가기, 뒤로가기 실행 시 캐시된 페이지를 보여주는 기법
- Shared Element Transitions: 페이지 라우팅이 일어났을 때, 두 페이지에 동일 요소가 있다면 해당 콘텍스트를 유지해 부드럽게 전환되게 하는 기법
- 현대의 서버 사이드 렌더링
- CSR과 SSR의 장점을 모두 취한 방식으로 작동하게 변화 중
- 최초 웹 사이트 진입 시에는 서버에서 완성된 HTML 제공, 이 후 라우팅에서는 서버에서 내려받은 자바스크립트를 바탕으로 싱글 페이지 애플리케이션처럼 작동
- Next.js, Remix
4.2 서버 사이드 렌더링을 위한 리액트 API 살펴보기
- 리액트 앱을 서버에서 렌더링 할 수 있는 API : Node.js 같은 서버 환경에서만 실행 가능
4.2.1 renderToString
- 인수로 넘겨 받은 리액트 컴포넌트를 렌더링해 HTML 문자열로 반환하는 함수
- 서버 사이드 렌더링에서 최초의 페이지를 HTML로 먼저 렌더링하는 역할
4.2.2 renderToStaticMarkup
- renderToString 함수와 유사
- 차이점은 리액트에서만 사용하는 추가적인 DOM 속성은 만들지 않음
4.2.3 renderToNodeStream
- renderToString과 결과물이 완전히 동일
- 차이점은 renderToNodeStream은 브라우저에서 사용이 불가능하고 결과물의 타입이 string이 아니라 ReadableStream 이다.
- 청크 타입으로 분리되어 내려오기 때문에 서버의 부담을 덜 수 있어서 대부분의 프레임워크에 사용되고 있다.
4.2.4 renderToStaticNodeStream
- renderToNodeStream 과 제공하는 결과물은 동일하나 리액트 속성이 제공되지 않는다.
4.2.5 hydrate
HTML 코드와 JS 코드를 서로 매칭시켜 동적인 웹사이트를 브라우저에 랜더링하는 기술
- renderToString과 renderToNodeStream 으로 생성된 HTML 콘텐츠에 자바스크립트 핸들러나 이벤트를 붙이는 역할
4.3 Next.js 톺아보기
4.3.1 Next.js 란?
- vercel 에서 만든 리액트 기반 서버 사이드 렌더링 프레임워크
4.3.2 Next.js 시작하기
- _app.tsx 와 _document.tsx의 차이점
- _app.tsx
- Next.js를 초기화하는 파일로 Next.js 설정과 관련된 코드를 모아두는 곳
- 서버나 클라이언트에서 모두 실행 가능
- _document.tsx
- Next.js로 만드는 웹사이트의 뼈대가 되는 HTML 설정과 관련된 코드를 추가하는 곳
- 무조건 서버에서 실행 (이벤트 추가 불가)
- _app.tsx
- 라우팅
- []안의 내용은 변수로 처리
- 전개 연산자로 처리하면 배열로 들어간다.
- getServerSideProps가 있으면 서버 사이드 렌더링 페이지로 들어가고 없으면 빌드 시점에 미리 만들어버리는 페이지가 된다.
- api 디렉터리는 서버의 API를 정의하는 폴더
- 서버에서 내려주는 데이터를 조합해 BFF 형태로 활용할 때
- 완전한 풀스택 애플리케이션을 구축하고 싶을 때
- CORS 문제를 우회하고 싶을 때 사용 가능
4.3.3 Data Fetching
- 데이터 불러오기 전략
- getStaticPaths와 getStaticProps
- 게시판, 공지같이 사용자와 관계없이 정적으로 결정된 페이지를 보여줄 때 사용
- 두 가지는 세트로 함께 있어야 한다.
- getServerSideProps
- 응답값에 따라 페이지의 루트 컴포넌트에 props를 반환할 수도 혹은 다른 페이지로 리다이렉트시킬 수도 있다.
- 서버에서만 실행되기 때문에 발생되는 제약
- window.document 같이 브라우저에서만 접근할 수 있는 객체에는 접근 불가
- protocol과 domain 없이 fetch 요청이 불가 (자신의 호스트를 유추할 수 없기 때문)
- 여기서 에러가 발생하면 500.tsx 같은 에러 페이지로 리다이렉트
- 경로에 없는 라우팅을 했을 경우 404.tsx 페이지로 리다이렉트
- getInitialProps
- 레거시이므로 왠만하면 getStaticProps나 getServerSideProps를 사용하자
- 서버와 클라이언트 모두에서 실행될 수 있음
4.3.4 스타일 적용하기
- 전역 스타일
- resetCSS 등 전체에 공통으로 적용하고 싶은 스타일은 _app.tsx를 활용해서 필요한 스타일을 직접 import로 불러오자
- 컴포넌트 레벨 CSS
- [name].module.css 같은 명명 규칙으로 컴포넌트 단위로 CSS를 적용할 수 있다.
- SCSS와 SASS
- 위와 동일하게 작성 가능
- CSS-in-JS
- _document.js 에 코드를 추가해서 작성 (326p)
4.3.5 _app.tsx 응용하기
- Next.js로 만든 모든 서비스가 진입하는 최초 진입점
- app.getInitialProps 내부에 웹 서비스를 최초에 접근했을 때만 실행하고 싶은 내용을 담아둘 수 있다.
- userAgent 확인
- 사용자 정보와 같은 애플리케이션 전역에서 사용해야 하는 정보
4.3.6 next.config.js 살펴보기
- Next.js 실행에 필요한 설정을 추가할 수 있는 파일
- basePath : 기본적으로 앱을 실행했을 때 시작되는 path localhost:3000/docs
- swcMinify : swc를 이용해 코드를 압축할지 true/false
- poweredByHeader : 응답 헤더에 next.js 정보를 제공할지 결정 true/false
- redirects : 특정 주소를 다른 주소로 보내고 싶을 때 사용
- reactStrictMode : 리액트 엄격 모드 true/false
- assetPrefix : next에서 빌드된 결과물을 동일한 호스트가 아닌 다른 CDN에 업로드하려면 이 옵션에 CDN 주소를 명시
6장. 리액트 개발 도구로 디버깅하기
6.1 리액트 개발 도구란?
- react-dev-tools
6.2 리액트 개발 도구 설치
- 확장 프로그램으로 설치
6.3 리액트 개발 도구 활용하기
- 개발자 도구에 Components와 Profiler가 추가됨
6.3.1 컴포넌트
- 리액트 앱의 컴포넌트 트리 확인
- 컴포넌트의 구조뿐만 아니라 props와 내부 hooks등 다양한 정보 확인
- 컴포넌트 트리
- 트리 구조
- 함수 선언식과 함수 표현식으로 작성된 컴포넌트는 트리에 컴포넌트명이 표시된다.
- memo로 익명 함수 컴포넌트를 감싸거나 고차 컴포넌트로 감싼 컴포넌트는 컴포넌트명이 표시되지 않는다.
- 그런 경우에는 Component.displayName = '어떤 컴포넌트' 로 작성하는 방법이 있다.
- 컴포넌트명과 props, rendered by
- 눈 모양 아이콘 → Elements 탭으로 이동
- 벌레 모양 아이콘 → Console 탭에 해당 컴포넌트가 찍힘
- {} 모양 아이콘 → Source 탭으로 이동
6.3.2 프로파일러
- 리액트의 렌더링 과정에서 어떤 컴포넌트가 렌더링됐는지, 또 몇 차례나 렌더링이 일어났으며 어떤 작업에서 오래 걸렸는지 등 추적
- 첫번째 동그라미는 프로파일링 시작 버튼
- 두번째 동그란 화살표는 새로고침 후 프로파일링 시작버튼
- 세번째 금지 표시는 프로파일링 종료 및 삭제
- 네번째 다섯번째는 불러오기, 저장하기
- 여섯번째 불꽃 모양은 렌더 커밋별로 어떤 작업이 일어났는지 기록, 어떤 컴포넌트가 렌더링이 되었는지, 얼마나 오래 걸렸는지 측정 가능
- 일곱번째 차트 아이콘은 렌더링 시간이 가장 오래걸린 컴포넌트를 순서대로 나열
- 여덟번째 달력 아이콘은 타임라인으로 시간이 지남에 따라 컴포넌트에서 어떤 일이 일어났는지 확인
8장. 좋은 리액트 코드 작성을 위한 환경 구축하기
8.1 ESLint를 활용한 정적 코드 분석
8.1.1 ESLint 살펴보기
- ESLint는 어떻게 코드를 분석할까?
- 자바스크립트 코드를 문자열로 읽는다.
- 자바스크립트 코드를 분석할 수 있는 파서(parser)로 코드를 구조화한다.
- 2번에서 구조화한 트리를 AST(Abstract Syntax Tree)라 하며, 이 구조화된 트리를 기준으로 각종 규칙과 대조한다.
- 규칙과 대조했을 때 이를 위반한 코드를 알리거나 수정한다.
- → 한 줄 밖에 안되는 코드가 JSON으로 이렇게 큰 트리가 되는 걸 보니 husky를 이용한 커밋이 왜 그렇게 오래 걸렸는지 알겠다..
8.1.2 eslint-plugin과 eslint-config
- eslint-plugin
- lint 규칙들을 모아놓은 패키지
- eslint-config
- eslint-plugin을 묶어서 완벽하게 한 세트로 제공하는 패키지
- 일부 IT 기업들에서 공개한 잘 만들어진 eslint-config
- eslint-config-airbnb
- @titicaca/triple-config-kit
- eslint-config-next
8.1.3 나만의 ESLint 규칙 만들기
- 이미 존재하는 규칙을 커스터마이징해서 적용하기: import React를 제거하기 위한 ESLint 규칙 만들기
- 완전히 새로운 규칙 만들기: new Date를 금지시키는 규칙
8.1.4 주의할 점
- Prettier와의 충돌
- 코드 포매팅 도구인 Prettier와 비슷한 기능도 ESLint가 제공하기 때문에 충돌이 발생할 수 있다.
- ESLint 에서 코드 포매팅에 관련된 규칙을 끄는 것으로 해결이 가능하다.
- 규칙에 대한 예외 처리, 그리고 react-hooks/no-exhaustive-deps
- useEffect와 useMemo에서 의존성 배열이 필요할 때, 위 규칙을 끄는 경우가 있다.
- 하지만 위험한 발상이며, 잠재적인 버그를 야기할 수 있다.
- 의존성 배열이 없어도 되는 경우라면 컴포넌트의 상태와 별개로 동작하기 때문에 선언 위치를 다시 고민해 보자.
- 의존성 배열이 너무 긴 경우라면 useEffect 내부 함수 내용이 너무 길다는 것과 동일하다. 함수를 쪼개자.
- 마운트 시점에 한번만 실행하고 싶은 경우라면 []로 의존성 배열을 작성하곤 한다. 이 방법은 클래스형 접근방법이고 함수형에는 맞지 않을 수 있다.
- ESLint 버전 충돌
8.2 리액트 팀이 권장하는 리액트 테스트 라이브러리
- 프론트엔드 테스트는 일반적인 사용자와 동일하거나 유사한 환경에서 수행
- 사용자가 프로그램에서 수행하는 주요 비즈니스 로직이나 모든 경우의 수를 고려
8.2.1 React Testing Library란?
- DOM Testing Library를 기반으로 만들어진 테스팅 라이브러리
8.2.2 자바스크립트 테스트의 기초
- 테스트할 함수나 모듈을 선정한다.
- 함수나 모듈이 반환하길 기대하는 값을 적는다.
- 함수나 모듈의 실제 반환 값을 적는다.
- 3번의 기대에 따라 2번의 결과가 일치하는지 확인한다.
- 기대하는 결과를 반환한다면 테스트는 성공이며, 만약 기대와 다른 결과를 반환하면 에러를 던진다.
- 테스팅 프레임워크
- Jest
- Mocha
- Karma
- Jasmine
- assertion 라이브러리
- should.js
- expect.js
- chai
8.2.3 리액트 컴포넌트 테스트 코드 작성하기
- 정적 컴포넌트인 경우 (무상태 컴포넌트)
- beforeEach : 각 테스트를 수행하기 전에 실행하는 함수
- describe : 비슷한 속성을 가진 테스트를 하나의 그룹으로 묶는 역할. 필수는 아니지만 가독성을 위한 코드
- it : test의 축약어
- testId : get 등의 선택자로 선택하기 어렵거나 곤란한 요소를 선택하기 위해 사용
- 동적 컴포넌트인 경우 (useState를 이용해 상태값을 관리하는 컴포넌트)
- setup 함수: 내부에서 컴포넌트를 렌더링하고 테스트에 필요한 button과 input을 반환
- userEvent.type : 사용자가 타이핑하는 것을 흉내내는 메서드
- fireEvent 보다 조금 더 자세한 테스트가 가능
- jest.spyOn : 어떠한 특정 메서드를 오염시키지 않고 실행이 됐는지, 또 어떤 인수로 실행됐는지 등 실행과 관련된 정보만 얻고 싶을 때 사용
- mockImplementation : 해당 메서드에 대한 모킹 구현.
- 비동기 이벤트가 발생하는 컴포넌트
- msw의 rest 활용
8.2.4 사용자 정의 훅 테스트하기
- react-hooks-testing-library
- renderHook
8.2.5 테스트를 작성하기에 앞서 고려해야 할 점
- 프론트엔드 테스트는 커버리지가 100%라고 하더라도 유저의 입력이 자유롭기 때문에 모든 상황을 커버하기 어렵다.
- 애플리케이션에서 가장 취약하거나 중요한 부분을 파악하는 것이 가장 중요
8.2.6 그 밖에 해볼 만한 여러가지 테스트
- 유닛 테스트: react-testing-library
- 통합 테스트: react-testing-library
- 엔드 투 엔드 테스트: cypress
8.2.7 정리
- 결국은 애플리케이션이 비즈니스 요구사항을 충족하는지 확인하는게 목표
10장. 리액트 17과 18의 변경사항 살펴보기
10.1 리액트 17버전 살펴보기
- 16버전과 다르게 새롭게 추가된 기능은 없다.
- 16 → 17 업그레이드는 큰 변화를 겪지 않고 순조롭게 마무리 가능
10.1.1 리액트의 점진적인 업그레이드
- 버전업은 실무, 현업에서 매우 번거로운 작업
- 전체 웹 서비스가 새로운 버전으로 완전히 넘어가버리거나, 계속 현재(과거 버전)에 머물러 있어야 한다.
- 그런데 리액트 17버전부터는 점진적인 업그레이드가 가능해진다.
- 리액트 17을 설치하고 이후에 리액트 18로 업데이트 한다면 리액트 18에서 제공하는 대부분의 기능을 사용할 수 있지만, 일부 기능에 대해서는 리액트 17에 머물러 있는 것이 가능
- 리액트 17 앱은 내부에서 리액트 16 앱을 게으르게 불러오기 때문에 가능
10.1.2 이벤트 위임 방식의 변경
- 리액트에서 이벤트는 어떻게 추가되는가?
- onClick 으로 이벤트를 추가한 경우
- click에 noop (no operation) 이라는 함수가 달려있고 아무런 일도 하지 않음.
- useRef를 활용해 고전적인 이벤트 핸들러 추가 방식으로 추가한 경우
- 이벤트 리스너에 click 으로 추가됨
- onClick 으로 이벤트를 추가한 경우
- → 리액트 16에서는 모든 이벤트가 document에 달려 있고, 리액트 17에서는 컴포넌트 루트에 달려있다.
10.1.3 import React from ‘react’ 가 더 이상 필요 없다: 새로운 JSX transform
- 리액트 17부터는 바벨과 협력해 import 구문 없이도 JSX를 변환할 수 있게 됐다.
- 번들링 크기를 약간 줄일 수 있다는 장점
10.1.4 그 밖의 주요 변경 사항
- 이벤트 풀링 제거
- 리액트는 브라우저 기본 이벤트가 아닌 한번 래핑한 이벤트 (SyntheticEvent)를 사용해 이벤트가 발생할 때 마다 이 이벤트를 새로 만들어야 했다.
- useEffect 클린업 함수의 비동기 실행
- 16 버전까지는 클린업 함수가 동기적으로 처리.
- → 클린업 함수가 완료되기 전까지는 다른 작업을 방해
- 17 버전부터는 화면이 완전히 업데이트된 이후에 클린업 함수가 비동기적으로 실행
- 16 버전까지는 클린업 함수가 동기적으로 처리.
- 컴포넌트의 undefined 반환에 대한 일관적인 처리
- 리액트 16에서는 forwardRef나 memo에서 undefined 를 반환하면 별다른 에러가 발생하지 않았지만, 17버전에서는 의도치 않게 잘못된 반환으로 인한 실수를 방지하기 위해 에러 발생 시킴. 18부터는 에러 발생 안함. ???
10.2 리액트 18 버전 살펴보기
10.2.1 새로 추가된 훅 살펴보기
- useId
- 컴포넌트별로 유니크한 값을 생성하는 새로운 훅
- 하나의 컴포넌트가 여러 군데에서 재사용되는 경우도 고려해야 하며, 리액트 컴포넌트 트리에서 컴포넌트가 가지는 모든 값이 겹치지 않고 다 달라야 한다는 제약
- 서버사이드렌더링 환경에서 하이드레이션이 일어날 때, 서버와 클라이언트에서 동일한 값을 가져야 에러가 발생하지 않는 점!
- 서버사이드와 클라이언트사이드 간에 동일한 값이 생성되어 하이드레이션 이슈도 발생하지 않음
- useTransition
- UI 변경을 가로막지 않고 상태를 업데이트할 수 있는 리액트 훅
- 상태 업데이트를 긴급하지 않은 것으로 간주해 무거운 렌더링 작업을 미룰 수 있다.
- 타이핑으로 인해서 setState가 일어나는 경우, 타이핑이 끝날 때 까지, useTransition으로 지연시킨 상태 업데이트는 일어나지 않는다. → 오토컴플릿 같은거 만들 때, debounce 대신 써도 되려나..?
- useDeferredValue
- 리액트 컴포넌트 트리에서 리렌더링이 급하지 않은 부분을 지연할 수 있게 도와주는 훅
- 디바운스와 비슷
- 차이점은 디바운스는 고정된 지연 시간을 필요로 하지만 useDeferredValue는 고정된 지연 시간 없이 첫 번째 렌더링이 완료된 이후에 useDeferredValue로 지연된 렌더링 수행
- useSyncExternalStore
- tearing 현상을 해결하기 위한 훅
- useInsertionEffect
- CSS-in-js 라이브러리를 위한 훅
- DOM이 실제로 변경되기 전에 동기적으로 실행
- useLayoutEffect보다 먼저 실행
10.2.2 react-dom/client
- createRoot
- render 메서드를 대체할 새로운 메서드
- hydrateRoot
- 서버 사이드 렌더링 애플리케이션에서 하이드레이션을 하기 위한 새로운 메서드
10.2.3 react-dom/server
- renderToPipeableStream
- 리액트 컴포넌트를 HTML로 렌더링하는 메서드
- renderToReadableStream
- 웹 스트림을 기반으로 작동
10.2.4 자동 배치(Automatic Batching)
- 리액트가 여러 상태 업데이트를 하나의 리렌더링으로 묶어서 성능을 향상시키는 방법
- 18에서 업데이트된 기능인데 이걸 쓰고 싶지 않다면 flushSync를 사용하면 된다.
10.2.5 더욱 엄격해진 엄격 모드
- 리액트의 엄격 모드는 리액트 앱에서 발생할 수도 있는 잠재적인 버그를 찾는데 도움이 되는 컴포넌트
- 개발자 모드에서만 작동한다. (프로덕션 모드 X)
- 더 이상 안전하지 않은 특정 생명주기(componentWillMount, componentWillReceiveProps, componentWillUpdate)를 사용하는 컴포넌트에 대한 경고
- 문자열 ref 사용 금지
- findDOMNode에 대한 경고 출력
- 구 Context API 사용 시 발생하는 경고
- 예상치 못한 사이드 이펙트 검사
- CLASS - constructor, render, shouldComponentUpdate, getDerivedStateFromProps
- CLASS - setState의 첫번째 인수
- FUNCTION - body
- useState, useMemo, useReducer에 전달되는 함수
- 18에서 업데이트 될 부분
- useEffect 두번 실행
- 우리는 useEffect를 쓸 때 적절한 cleanup 함수를 배치해서 반복 실행될 수 있는 useEffect로 부터 최대한 자유로운 컴포넌트를 제작하자.
10.2.6 Suspense 기능 강화
- Suspense는 컴포넌트를 동적으로 가져올 수 있게 도와주는 기능 (리액트 16.6 버전에서는 실험 기능)
- React.lazy로 선언한 지연 컴포넌트를 받아오기 전에는 fallback을 보여주고, 완료되면 해당 컴포넌트를 보여주는 역할
10.2.7 인터넷 익스플로러 지원 중단에 따른 추가 폴리필 필요
- Promise: 비동기 연산이 종료된 이후에 실패 또는 결괏값을 확인할 수 있는 객체
- Symbol: 자바스크립트의 새로운 데이터 형식으로 익명의 객체 속성을 만들 수 있는 특성을 가진 객체
- Object.assign: 객체의 열거 가능한 모든 속성을 다른 객체로 붙여 넣는 메서드
- 이 세가지 기능이 지원되지 않는 브라우저를 고려해야 한다면 폴리필을 추가
10.2.9 정리
- 18 버전의 핵심은 동시성 렌더링
- 렌더링 중간에 일시 중지, 나중에 여유가 될 때 시작하거나 진행 중인 렌더링 작업을 포기하고 새로 다시 시작도 가능
- 동시성 모드를 활용하면서 추가적으로 라이브러리를 쓰기 위해서는 검토가 필요
11장. Next.js 13과 리액트 18
11.1 app 디렉터리의 등장
- Next.js 13버전은 Next.js 역사를 통틀어 가장 큰 변화가 있는 릴리스
- _document: 페이지에서 쓰이는 <html>과 <body>태그를 수정하거나 서버 사이드 렌더링 styled-components 같은 일부 CSS-in-JS를 지원하기 위한 코드를 삽입하는 제한적인 용도로 사용. 오직 서버에서만 작동하므로 onClick 같은 이벤트 핸들러 또는 클라이언트 로직 금지
- _app: 페이지를 초기화하기 위한 용도로 사용되며 다음과 같은 작업이 가능
- 페이지 변경 시에 유지하고 싶은 레이아웃
- 페이지 변경 시 상태 유지
- componentDidCatch 를 활용한 에러 핸들링
- 페이지간 추가적인 데이터 삽입
- global CSS 주입
- 즉 Next.js 12 버전까지 페이지 공통 레이아웃을 유지할 수 있는 방법은 _app이 유일
- 이러한 한계를 극복하기 위해서 Next.js 13 버전에서 app 레이아웃이 나왔다.
11.1.1 라우팅
- 기존에 pages로 정의하던 라우팅 방식이 app 디렉터리로 이동
- 파일명으로 라우팅하는 것이 불가능, 폴더명까지만 주소로 변환
- layout.js
- app 디렉터리 내부의 폴더에 포함될 수 있는 파일명을 몇 가지로 제한
- 페이지의 기본 레이아웃을 구성하는 요소
- 루트 레이아웃에는 기존 _app, _document를 대신해 웹페이지를 시작하는데 필요한 공통 코드를 삽입할 수 있다.
- page.js
- layout을 기반으로 리액트 컴포넌트를 노출
- page가 받는 props
- params: 옵셔널 값으로 […id]와 같은 동적 라우트 파라미터를 사용할 경우 해당 파라미터에 값이 들어온다.
- searchParams: url에서 쿼리파라미터
- error.js
- 공통 에러 컴포넌트
- 특정 라우팅별로 서로 다른 에러 UI 렌더링이 가능
- error.js 가 받는 props
- error 객체
- reset 함수
- not-found.js
- 특정 라우팅 하위의 주소를 찾을 수 없는 404 페이지 렌더링
- 서버 컴포넌트로 구성
- loading.js
- Suspense를 기반으로 해당 컴포넌트가 불러오는 중임을 나타낼 때 사용
- 클라이언트 컴포넌트로 사용도 가능
- route.js
- api 디렉터리에서 라우팅 주소를 담당하며 디렉터리의 파일명이 route.js로 통일
- HTTP method 정의
11.2 리액트 서버 컴포넌트
11.2.1 기존 리액트 컴포넌트와 서버 사이드 렌더링의 한계
- 기존 리액트의 모든 컴포넌트는 클라이언트에서 작동하며 브라우저에서 자바스크립트 코드 처리가 이뤄진다. : 클라이언트 사이드 렌더링
- 코드 다운로드 → 리액트 컴포넌트 트리 생성 → DOM에 렌더링
- 서버 사이드 렌더링?
- 미리 서버에서 DOM을 만들어 오고 → 이 DOM을 기준으로 하이드레이션 진행
- 서버 사이드 렌더링의 한계
- 자바스크립트 번들 크기가 0인 컴포넌트를 만들 수 없다.
- 게시판 등 사용자가 작성한 HTML에 위험한 태그를 제거하기 위해 사용되는 sanitize-html 라이브러리
- 63.3kb에 달하는 용량
- 클라이언트 사이드에서 렌더링하면 사용자 기기의 부담이 됨
- 백엔드 리소스에 대한 직접 접근이 불가
- REST API를 사용하게 되면 백엔드에서 항상 클라이언트에서 데이터에 접근하기 위한 방법을 마련해야 한다는 단점
- 클라이언트에서 직접 import db로 데이터베이스에 직접 액세스하거나 파일시스템에 접근하는게 편하다.
- 자동 코드 분할이 불가능
- 거대한 코드 번들 대신 코드를 여러 작은 단위로 나누어 필요할 때만 동적으로 지연 로딩
- 일반적으로 리액트에서는 lazy를 사용해서 구현
- 연쇄적으로 발생하는 클라이언트와 서버의 요청을 대응하기 어려움
- 추상화에 드는 비용이 증가
11.2.2 서버 컴포넌트란?
- 하나의 언어, 하나의 프레임워크, 그리고 하나의 API와 개념을 사용하면서 서버와 클라이언트 모두에서 컴포넌트를 렌더링할 수 있는 기법
- 클라이언트 컴포넌트는 서버 컴포넌트를 import 할 수 없음
- 서버 컴포넌트
- 요청이 오면 그 순간 서버에서 딱 한번 실행될 뿐이므로 상태를 가질 수 없다. (useState, useReducer 사용 불가)
- 렌더링 생명주기도 없다. (useEffect, useLayoutEffect 사용 불가)
- window.document에 접근 불가
- 서버에만 있는 데이터를 async/await 로 접근할 수 있음
- div span p 같은 요소를 렌더링하거나 클라이언트 컴포넌트를 렌더링할 수 있다.
- 클라이언트 컴포넌트
- 서버 컴포넌트, 서버 전용 훅, 유틸리티를 불러올 수 없다.
- 서버 → 클라이언트 → 서버 컴포넌트의 구조는 가능
11.2.3 서버 사이드 렌더링과 서버 컴포넌트의 차이
- 서버 컴포넌트와 서버 사이드 렌더링은 완전히 다른 개념
- 서버 사이드 렌더링
- 응답받은 페이지 전체를 HTML로 렌더링하는 과정을 서버에서 수행한 후 그 결과를 클라이언트에 내려준다.
- 이 후 클라이언트에서 하이드레이션 과정을 거쳐 서버의 결과물을 확인하고 이벤트를 붙이는 등의 작업 수행
- 초기에 인터랙션은 불가하지만 정적인 HTML을 빠르게 내려주는데 초점
- 서버 사이드 렌더링과 서버 컴포넌트를 모두 채택하는 것도 가능
- 둘은 대체재가 아닌 상호보완하는 개념
11.2.4 서버 컴포넌트는 어떻게 작동하는가?
- 서버가 렌더링 요청을 받는다. 루트 컴포넌트는 항상 서버 컴포넌트다.
- 컴포넌트를 JSON으로 직렬화한다.
- 브라우저가 리액트 컴포넌트 트리를 구성. 최종적으로 이 트리를 렌더링해 브라우저의 DOM에 커밋
- 서버 컴포넌트 특별한 점
- 스트리밍 형태로 보냄으로써 클라이언트가 줄 단위로 JSON을 읽고 컴포넌트를 렌더링할 수 있어서 빠르게 결과물을 보여줄 수 있음
- 각 컴포넌트별로 번들링이 별개로 되어 필요에 따라 지연해서 받는 작업이 가능
- 서버 사이드 렌더링과는 다르게 결과물이 HTML이 아닌 JSON형태로 보내짐. 이유는 단순히 HTML을 그리는 작업 이상의 일을 필요로 하기 때문.
11.3 Next.js에서의 리액트 서버 컴포넌트
11.3.1 새로운 fetch 도입과 getServerSideProps, getStaticProps, getInitialProps의 삭제
- 모든 데이터 요청은 웹에서 제공하는 표준 API인 fetch를 기반으로 이뤄진다.
11.3.2 정적 렌더링과 동적 렌더링
- 정적 라우팅에 대해서는 기본적으로 빌드 타임에 렌더링을 미리 해두고 캐싱해서 재사용할 수 있게
- 동적 라우팅에 대해서는 서버에 매번 요청이 올 때 마다 컴포넌트를 렌더링하도록 변경
- 동적이지만 특정 주소에 대해서 캐싱하고 싶은 경우에는 generateStaticParams를 사용 (구. getStaticPaths)
11.3.3 캐시와 mutating, 그리고 revalidating
- fetch의 기본 작동을 재정의해 해당 데이터의 유효한 시간을 정해두고 이 시간이 지나면 다시 데이터를 불러와서 페이지를 렌더링 하는 방법? → revalidate 라는 변수 선언
export const revalidate = 60
- 이렇게 선언해두면 하위에 있는 모든 라우팅에서는 페이지를 60초 간격으로 갱신해 새로 렌더링
- 캐시를 전체적으로 무효화하고 싶을 때는 router.refresh() 사용
11.3.4 스트리밍을 활용한 점진적인 페이지 불러오기
- 과거의 서버 사이드 렌더링은 페이지가 다 불러와질 때까지 사용자는 빈 화면을 보게 된다.
- 스트리밍을 활용하면 모든 데이터가 로드될 때까지 기다리지 않더라도 먼저 데이터가 로드되는 컴포넌트를 빠르게 보여주는 방법이 가능하다.
- 핵심 웹 지표인 FCP 개선에 큰 도움을 준다.
- 스트리밍을 활용할 수 있는 방법
- 경로에 loading.tsx 배치
- Suspense 배치
11.4 웹팩의 대항마, 터보팩의 등장(beta)
- SWC는 많은 프로젝트에서 바벨을 대신해 사용되고 있다.
- Next.js 13 에서는 터보팩이 출시됐다.
- 터보팩은 웹팩 대비 최대 700배, vite 대비 최대 10배 빠르다.
- 러스트를 기반으로 작성
- 개발 모드에서만 제한적으로 사용이 가능
11.5 서버 액션(alpha)
- API를 굳이 생성하지 않더라도 함수 수준에서 서버에 직접 접근해 데이터 요청 등을 수행할 수 있는 기능
- 이 서버 액션을 활성화하려면 next.config.js에서 실험 기능 활성화 필요
- ‘use server’ 선언하고 함수는 async로 호출
11.5.1 form의 action
- form action에 달아주는 함수를 서버 액션으로 만들어주게 되면 해당 이벤트를 발생시키는 것은 클라이언트지만 실제로 함수 자체가 수행되는 것은 서버가 된다.
- 클라이언트 번들링 결과물에는 해당 함수가 포함되지 않고 서버에서만 실행됨
- 폼과 실제 노출하는 데이터가 연동되어 있을 때 더욱 효과적으로 쓸 수 있다.
- 데이터 추가 및 수정 요청을 좀 더 자연스럽게 수행할 수 있고, 캐시를 효과적으로 초기화할 수 있다.
11.5.2 input의 submit과 image의 formAction
- form action과 동일하게 서버 액션을 추가할 수 있다.
11.5.3 startTransition과의 연동
- useTransition에서 제공하는 startTransition에서도 서버 액션을 활용할 수 있다.
- page 단위의 loading.jsx를 사용하지 않아도 된다는 장점 ⇒ isPending을 활용해 startTransition으로 서버 액션이 실행됐을 때, 해당 버튼을 숨기고 로딩 버튼을 노출해서 페이지 단위가 아닌 컴포넌트 단위의 로딩 처리가 가능
11.5.4 server mutation이 없는 작업
11.5.5 서버 액션 사용 시 주의할 점
- 클라이언트 컴포넌트 내에 정의될 수 없다.
- 서버 액션을 클라이언트 컴포넌트에 넘길 수 있다. (서버 컴포넌트에서 클라이언트 컴포넌트를 불러올 수 있는 것과 동일한 원리)
11.6 그 밖의 변화
- SEO를 쉽게 작성할 수 있는 기능이 추가
- 라우트 미들웨어 강화
- 정적으로 내부 링크를 분석할 수 있는 기능
11.7 Next.js 13 코드 맛보기
11.7.1 getServerSideProps와 비슷한 서버 사이드 렌더링 구현해 보기
11.7.2 getStaticProps와 비슷한 정적인 페이지 렌더링 구현해 보기
11.7.3 로딩, 스트리밍, 서스펜스
- loading과 Suspense 모두 동일한 방식으로 작동하며 Suspense가 조금 더 개발자가 원하는 형태로 쪼개서 보여줄 수 있다.
'concept > React, Redux, RN' 카테고리의 다른 글
Tanstack-Query (a.k.a React-Query) 입문자들을 위한 간단 설명 (0) 2024.04.14 [Udemy] 클린코드 리액트 - 소개, State (0) 2024.02.04 채팅 기능 구현을 위한 web socket과 sse 방식 POC (0) 2023.11.06 이미지 최적화를 위한 방법들과 Next/Image (0) 2023.11.05 React에서 DOM에 직접 접근보다 useRef 쓰는 이유 (0) 2022.09.09 - 원시 타입