모던 리액트 Deep Dive

Singsoong·2024년 9월 5일
0

독서

목록 보기
2/2
post-thumbnail

모던 리액트 Deep Dive 책을 읽고 Remind할 내용들을 정리했습니다.


들어가며

  • 리액트를 선호하는 이유
    • 리액트는 단방향 바인딩만 지원
      - 데이터의 흐름이 한쪽으로만 간다
      - 양방향으로 바인딩되면 뷰의 변화가 컴포넌트에 영향을 미칠수도, 반대로 컴포넌트의 상태가 변경되면 뷰의 상태도 변할 수 있음

      // 리액트의 경우 name이 변경되는 경우는 setName이 호출될 때 뿐이다.
      // name이 변경된 이유를 찾고 싶다면 setName이 호출된 곳을 찾으면 된다.
      function App {
      	const [name, setName] = useState('');
      
      function onChange(e) {
      	setName(e.target.value);
      }
    • JSX(JavaScript XML) 사용

      • Angular는 Angular에서만 사용하는 전용 문법(ngIf)을 익혀야 했지만, 리액트는 HTML에 자바스크립트 문법을 더한 JSX를 사용하여 익히기 쉽다.
    • 강력한 커뮤니티

1장: 리액트 개발을 위해 꼭 알아야 할 자바스크립트

자바스크립트의 동등 비교

자바스크립트의 모든 값은 데이터 타입을 갖고 있다.

  • 원시 타입: boolean, null, undefined, number, string, symbol, bigint
  • 객체 타입: object

null이 가지고 있는 특별한 점 하나는 다른 원시값과 다르게 typeof(null) 확인했을 때, null 타입이 아니라 object 라는 결과가 반환된다.

빈 객체, 빈 배열은 truthy 한 값이다.

Number 타입은 -2의 53승 ~ 2의 53승 까지 다룰 수 있다.

Symbol 은 ES6에서 새롭게 추가된 타입으로, 중복되지 않은 어떠한 고유한 값을 나타내기 위해 만들어졌다. 심벌은 심벌 함수를 이용해서만 만들 수 있다.

const key = Symbol('key')
const key2 = Symbol('key')

key === key2 // false

Symbol.for('hello') === Symbol.for('hello') // true

원시 타입과 객체 타입의 가장 큰 차이점은 저장하는 방식의 차이다. 원시 타입은 불변 형태의 값으로 저장되고 이 값은 변수 할당 시점에 메모리 영역을 차지하고 저장된다.

객체는 값을 저장하는게 아니라 참조를 저장하기 때문에 동일하게 선언했던 객체라 하더라도 저장하는 순간 다른 참조를 바라본다.

리액트에서의 동등 비교

리액트에서 사용하는 동등 비교는 ==나 ===가 아닌 Object.is 이다.

  1. 주어진 객체의 키를 순회하면서 두 값이 엄격한 동등성을 가지는지 확인
  2. 두 객체 간의 모든 키의 값이 동일하다면
  3. true 반환

요약하자면, Object.is 로 먼저 비교를 수행한 다음에 객체 간 얕은 비교를 한 번 더 수행한다.

객체 간 얕은 비교란 객체의 첫 번째 깊이에 존재하는 값만 비교한다는 것을 의미

객체의 얕은 비교까지만 구현한 이유는?

먼저, 리액트에서 사용하는 JSX Props는 객체이고, 여기에 있는 props만 일차적으로 비교하면 되기 때문

(기본적으로 리액트는 props에서 꺼내온 값을 기준으로 렌더링을 수행하기 때문에 일반적인 케이스에서는 얕은 비교로 충분할 것, 이러한 특성을 안다면 props에 또 다른 객체를 넘겨준다면 리액트 렌더링이 예상치 못하게 작동한다는 것을 알 수 있다.)

함수 호이스팅

  • 함수 호이스팅이라 함은, 함수 선언문이 마치 코드 맨 앞단에 작성된 것처럼 작동하는 자바스크립트의 특징
  • 함수 할당식 (함수를 변수에 할당)하면 변수는 호이스팅 되지만 할당한 함수는 호이스팅되지 않는다.
hello() // hello is not a function

let hello = function () {
	console.log('hello')
}

hello()
  • 함수 선언식, 함수 할당식 중에 좋은것은 없고, 관리해야 하는 스코프가 길어질 경우 함수 할당식이 관리하기 더 편할 것이다. 프로젝트의 상황에 맞는 작성법을 일관되게 사용하자.

즉시 실행 함수

  • 말 그대로 함수를 정의하고 그 순간 즉시 실행되는 함수
  • 단 한번만 호출되고, 다시금 호출할 수 없는 함수
  • 글로벌 스코프를 오염시키지 않는 독립적인 함수 스코프를 운용할 수 있다.
  • 코드를 읽는 이로 하여금 이 함수는 어디서든 다시금 호출되지 않는다는 점을 각인 시킬 수 있다.
(function (a, b) {
	return a + b
})(10, 24);

// 일반적으로 이름을 붙히지 않는다
((a,b) => {
	return a + b 
	},
)(10, 24)

함수를 만들 때 주의사항

  1. 사이드 이펙트를 최대한 억제하자.
  • useEffect 최소화
  1. 함수를 작게 만들자
  • 함수는 하나의 일을, 함수의 사이즈는 작게
  1. 함수의 이름은 누구나 이해할 수 있게

스코프

// 자바스크립트는 기본적으로 함수 레벨 스코프를 따른다

if (true) {
	const a = "text"
}

console.log(a) // "text"

function hello() {
	let b = "text"
}

console.log(b); // b is not defined

클로저

function outerFunction() {
    let x = "hello"
    function innerFunction(){
        console.log(x);
    }
    return innerFunction
}

const innerFunction = outerFunction();
innerFunction(); // "hello"

outerFunction은 innerFunction을 반환하며 실행을 종료했다. 반환한 함수에는 x라는 변수가 존재하지 않지만, 해당 함수가 선언된 어휘적 환경, 즉 outerFunction에는 x라는 변수가 존재하며 접근할 수 있다.

클로저를 사용하는 데는 비용이 든다. 클로저는 생성될 때 마다 그 선언적 환경을 기억해야 하므로 추가로 비용이 발생한다.

태스크 큐와 마이크로 태스크 큐

태스크 큐는 비동기 작업을 수행하는 큐이다. 태스트 큐와 다르게 마이크로 태스크 큐는 기존 태스크 큐보다 우선권을 갖는다.

마이크로 태스크 큐에는 대표적으로 Promise가 있다. 태스크 큐에 들어가는 setInterval, setTimeout보다 Promise가 먼저 실행된다. 마이크로 태스크 큐가 빌 때까지는 기존 태스크 큐의 실행은 뒤로 미뤄진다.

  • 태스크 큐: setTimeout, setInterval, setImmediate
  • 마이크로 태스크 큐: process.nextTick. Promises, queueMicroTask, MutationObserver
console.log(a);

setTimeout(()=>{console.log('b')},0)

Promise.resolve().then(()=>{ console.log('c')})

window.requestAnimationFrame(()=>{
    console.log('d')
})

위 코드를 실행하면 a, c, d, b 순서로 출력된다. 브라우저에 렌더링 하는 작업은 마이크로 태스크 큐와 태스크 큐 사이에서 일어난다는 것을 확인할 수 있다.

바벨(babel)을 사용하는 이유

바벨은 자바스크립트의 최신 문법을 다양한 브라우저에서도 일관적으로 지원할 수 있도록 코드를 트랜스파일한다.

배열 구조 분해 할당 - ES6(ES2015)

const array = [1, 2, 3, 4, 5]
const [first, second, third, ...arrayRest] = array

// first 1
// second 2
// third 3
// arraryRest [4, 5]

// 기본값을 선언할 수도 있음, undefined일 때에만 기본값을 사용함
const array = [1, 2]
const [a = 10, b = 10, c = 10] = array
// a 1
// b 2
// c 10

객체 구조 분해 할당 - ECMA2018

const object = {
	a: 1,
	b: 1,
	c: 1,
	d: 1,
	e: 1,
}

const { a, b, c, ...objectRest } = object
// a 1
// b 1
// c 1
// objectRest = {d: 1, e: 1}

useState가 객체가 아닌 배열을 반환하는 이유는 객체 구조 분해 할당은 사용하는 쪽에서 원하는 이름으로 변경하는 것이 번거롭다.

객체 전개 구문

객체 전개 구문에 있어서 순서 중요, 전개 구문 이후에 값 할당이 있다면 전개 구문이 할당한 값을 덮어쓰겠지만 반대의 경우 오히려 전개 구문이 해당 값을 덮어쓰는 일이 벌어진다.

Array의 프로토타입 메서드

  • reduce: 콜백함수와 초깃값을 추가로 줌
const arr = [1, 2, 3, 4, 5]
const sum = arr.reduce((result, item) => {
	return result + item
}, 0)

// 15
  • forEach: map과는 다르게 아무런 반환값이 없다. 또한 break, return 그 무엇을 사용해도 배열 순회를 멈출 수 없다.

타입스크립트

  • any 대신 unknown을 사용하자
  • tsconfig.json 작성하기
{
	"compilerOptions": {
		"outDir": "./dist",
		"allowJs": true,
		"target": "es5",
	},
	"include": ["./src/**/*"]
}

// outDir은 .ts나 .js가 만들어진 결과를 넣어두는 폴더.
// tsc는 타입스크립트를 자바스크립트로 변환하는 명령어, tsc를 사용하면 결과물이 outDir로 넘어감
// allowJs는 .js 파일을 허용할 것인지 여부
// target에는 결과물이 될 자바스크립트 버전을 지정
// include에는 트랜스파일할 자바스크립트와 타입스크립트 파일을 지정

2장: 리액트 핵심 요소 깊게 살펴보기

JSX

  • jsx는 리액트가 등장하면서 메타에서 소개한 새로운 구문이지만 반드시 리액트에서만 사용하라는 법은 없다.
  • jsx는 자바스크립트 표준이 아니므로 트랜스파일러를 거쳐야 한다.
  • jsx는 자바스크립트 내부에서 표현하기 까다로웠던 XML 스타일의 트리 구문을 작성하는데 많은 도움을 주는 새로운 문법
  • 요소명은 HTML 태그명과 구분짓기 위해 대문자로 시작해야 한다.
  • @babel/plugin-transform-react-jsx 플러그인에 의해 jsx가 자바스크립트로 변환된다.

DOM과 브라우저 렌더링 과정

DOM은 웹페이지에 대한 인터페이스로 브라우저가 웹페이지의 콘텐츠와 구조를 어떻게 보여줄지에 대한 정보를 담고 있다. 브라우저가 웹사이트 접근 요청을 받고 화면을 그리는 과정은 아래와 같다.

  1. 브라우저가 사용한 요청한 주소를 방문해 HTML 파일 다운
  2. 브라우저의 렌더링 엔진은 HTML 파싱 → DOM 노드로 구성된 트리(DOM)을 만듦
  3. 2번 과정에서 CSS 파일을 만나면 해당 CSS 파일도 다운
  4. 브라우저의 렌더링 엔진은 이 CSS도 파싱해 CSS 노드로 구성된 트리 (CSSOM)를 만듦
  5. 브라우저는 2번에서 만든 DOM 노드를 순회하는데, 여기서 모든 노드를 방문하는 것이 아니고 사용자 눈에 보이는 노드만 방문
    즉, display: none 과 같이 사용자 화면에 보이지 않는 요소는 방문해 작업하지 않음. 트리 분석 하는 과정을 조금이라도 빠르게하기 위함
  6. 5번에서 제외된, 눈에 보이는 노드를 대상으로 해당 노드에 대한 CSSOM 정보를 찾고 여기서 발견한 CSS 스타일링을 노드에 적용

가상 DOM의 탄생 배경

SPA(Single Page Application)이 나오기 전에는 페이지가 변경되는 경우 다른 페이지로 가서 처음부터 HTML을 새로 받아서 다시금 렌더링 과정을 시작했다. 하지만, SPA의 경우 하나의 페이지에서 계속해서 요소의 위치를 재계산하게 된다. DOM을 관리하는 과정에서 부담해야 할 비용이 커진다. 사용자의 인터렉션에 따라 DOM의 모든 변경 사항을 추적하는 것은 개발자 입장에서 너무나 수고스러운 일이다.

개발자는 인터랙션에 모든 DOM의 변경보다는 결과적으로 만들어지는 DOM 결과물 하나만 알고 싶을 것이다. 인터랙션에 따른 DOM의 최종 결과물을 간편하게 제공하는 것은 브라우저뿐만 아니라 개발자에게도 매우 유용하다. 이런 문제점을 해결하기 위해 탄생한 것이 가상 DOM이다.

  • 가상DOM은 실제 브라우저의 DOM이 아닌 리액트가 관리하는 가상의 DOM
  • DOM을 일단 메모리에 저장하고 리액트가 실제 변경에 대한 준비가 완료됐을 때 실제 DOM에 반영
  • DOM 계산을 브라우저가 아닌 메모리에서 계산하는 과정을 한 번 거치게 된다면 실제로는 여러 번 발생했을 렌더링 과정을 최소화 할 수 있음
  • 가상 DOM을 사용한다고 무조건 실제 DOM보다 빠른것이 아님

클래스형 컴포넌트의 한계

  • 데이터의 흐름을 추적하기 어렵다
  • 애플리케이션 내부 로직의 재사용이 어렵다
  • 기능이 많아질수록 컴포넌트의 크기가 커진다
  • 클래스는 함수에 비해 상대적으로 어렵다
  • 코드 크기를 최적화하기 어렵다

함수형 컴포넌트의 렌더링되는 경우

  • useState()의 setter가 실행되는 경우
  • useReducer()의 dispatch가 실행되는 경우
  • 컴포넌트의 key props가 변경되는 경우
  • props가 변경되는 경우
  • 부모 컴포넌트가 렌더링될 경우

컴포넌트의 key props를 추가해야 하는 이유

리액트에서 key는 리렌더링이 발생하는 동안 형제 요소들 사이에서 동일한 요소를 식별하는 값이다.

3장: 리액트 훅 깊게 살펴보기

리액트 훅 깊게 살펴보기

useState

게으른 초기화

// 일반적인 useState
const [count, setCount] = useState(1)

// 게으른 초기화
const [count, setCount] = useState(
() => Number.parseInt(window.localStorage.getItem(cacheKey)),
)
  • 게으른 초기화는 useState의 초깃값이 복잡하거나 무거운 연산을 포함하고 있을 때 사용
    • 무거운 연산: localStorage, sessionStorage에 대한 접근, map, filter, find 같은 배열에 대한 접근
  • 게으른 초기화 함수는 오로지 state가 처음 만들어질 때만 사용됨 → 이후 리렌더링이 되더라도 이 함수의 실행은 무시됨

useEffect

의존성 배열

// 1
function Component() {
	useEffect(()=>{})
}

의존성 배열에 아무런 값도 넘기지 않는다면 (빈 배열 조차도 넘기지 않는다면) 의존성을 비교할 필요 없이 렌더링할 때마다 실행이 필요하다고 판단해 렌더링이 발생할 때마다 실행됨

→ 일반적으로, 컴포넌트가 렌더링됐는지 확인하기 위한 방법으로 사용됨

// 2
function Component() { console.log('a') }

1번과 2번의 차이점

useEffect는 클라이언트 사이드에서 실행되는 것을 보장해준다. 다시 말해 1번은 컴포넌트가 렌더링이 완료되고 나서 실행되고 2번은 렌더링되는 도중에 실행된다. 따라서 2번의 경우 서버 사이드 렌더링의 경우에 서버에서도 실행된다.

얕은 비교

의존성 배열을 비교할 때 이전 값과 현재 값은 얕은 비교를 한다. Object.is를 기반으로 하는 얕은 비교를 수행한다.

함수명을 부여하자

useEffect의 수가 적거나 복잡성이 낮다면 첫 번째 인수로 익명 함수를 넘겨줘도 상관없지만, 코드가 복잡하고 많아질수록 무슨 일을 하는 useEffect 코드인지 파악하기 어려워진다. 이 때 useEffect의 인수를 익명 함수가 아닌 적절한 이름을 사용한 기명 함수로 바꾸자.

useEffect(
	function logActiveUser() {
		logging(user.id)
	},
	[user.id],
)

useRef

useState와 비교해서

  • 공통점: 컴포넌트 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장
  • 차이점: useRef는 반환값인 객체 내부에 있는 current로 값에 접근, 변경할 수 있음
    useRef는 그 값이 변하더라도 렌더링을 발생시키지 않음

일반적인 사용 예

function Component(){
	const inputRef = useRef()
	
	console.log(inputRef.current) // 렌더링 전이므로 undefined
	
	useEffect(() => {
		console.log(inputRef.current) // <input type="text />
	}, [inputRef])
	
	return <input ref={inputRef} type="text />
}

언제 사용?

원하는 시점의 값을 렌더링에 영향을 미치지 않고 보관해두고 싶다면 useRef를 사용하는것이 좋음

useContext

주의할 점

  • useContext를 함수형 컴포넌트 내부에서 사용할 때는 항상 컴포넌트 재활용이 어려워짐. useContext가 선언돼 있으면 Provider에 의존성을 가지고 있는 셈.
  • 컨텍스트가 미치는 범위는 필요한 환경에서 최대한 좁게 만들자

useReducer

useState의 심화 버전이라고 볼 수 있음

function reducer() {
	switch (action.type){
		case 'up':
			return { count: state.count + 1 }
		default:
			throw new Error('error');
			
		}
}

const initialState = { count: 0 };

function App(){
	const [state, dispatcher] = useReducer(reducer, initialState, init);
	
	function handleUpButtonClick(){
		dispatcher({ type: 'up' })
	}
	
	...
  • 목적: 복잡한 형태의 state를 사전에 정의된 dispatcher로만 수정할 수 있게 만들어 줌으로써 state 값에 대한 접근은 컴포넌트에서만 가능하게 하고, 이를 업데이트하는 방법에 대한 상세 정의는 컴포넌트 밖에다 둔 다음, state의 업데이트를 미리 정의해 둔 dispatcher로만 제한하는 것

useImperativeHandle

부모에게서 넘겨받은 ref를 원하는 대로 수정할 수 있는 훅

const Input = forwardRef((props, ref) => {
	useImperativeHandle(
		ref,
		() => ({
			alert: () => alert(props.value),
		}),
		[props.value],
	)
	
	return <input ref={ref} {...props} />
})

function App() {
	const inputRef = useRef()
	const [text, setText] = useState('');
	
	function handleClick() {
		inputRef.current.alert()
	}
	
	return (
		<>
			<Input ref={inputRef} value={text} />
	
	...
  • 자주 쓰지말라고 권고하고 있음

useLayoutEffect

모든 DOM의 변경 후에 useLayoutEffect의 콜백 함수가 동기적으로 발생

함수의 실행순서

  1. 리액트가 DOM을 업데이트
  2. useLayoutEffect를 실행
  3. 브라우저에 변경 사항을 반영
  4. useEffect를 실행

언제 사용?

동기적으로 발생한다는 것은 useLayoutEffect의 실행이 종료될 때까지 기다린 다음 그린다는 것을 의미함, 이 점을 유의하여 웹 어플리케이션 성능에 문제가 발생하지 않도록 유의

DOM은 계산됐지만 이것이 화면에 반영되기 전에 하고 싶은 작업이 있을 때 사용하자. (DOM 요소를 기반으로 한 애니메이션, 스크롤 위치 제어 등 화면에 반영되기 전에 하고 싶은 작업)

고차 컴포넌트

고차 컴포넌트는 컴포넌트 전체를 감쌀 수 있다는 점에서 사용자 정의 훅보다 더욱 큰 영향력을 컴포넌트에 미칠 수 있음

단순히 값을 반환하거나 부수 효과를 실행하는 사용자 정의 훅과는 다르게 컴포넌트의 결과물에 영향을 미칠 수 있는 다른 공통된 작업을 처리할 수 있음

사용자 정의 훅이 use로 시작했다면, 고차 컴포넌트는 with로 시작해야 하는 관습이 있음

주의 할점

고차 컴포넌트는 반드시 컴포넌트를 인수로 받게 되는데, 컴포넌트의 props를 임의로 수정, 추가, 삭제하는 일은 없어야함

여러 개의 고차 컴포넌트로 감싸면 복잡성이 커지므로 지양하고, 고차 컴포넌트는 최소한으로 사용하자

4장: 서버 사이드 렌더링

서버 사이드 렌더링의 장점

최초 페이지 진입이 비교적 빠름

서버가 사용자에게 렌더링을 제공할 수 있을 정도의 충분한 리소스가 확보되어있을 때의 경우임, 리소스를 확보하기 어렵다면 오히려 SPA보다 느려질 수 있음

메타데이터 제공이 쉬움

  • 검색 엔진 로봇은 HTML만 다운로드함, 자바스크립트 코드 실행하지 않음
  • 검색 엔진에 제공할 정보를 서버에서 가공해서 HTML 응답으로 제공할 수 있음

누적 레이아웃 이동이 적음

누적 레이아웃 이동이란 사용자에게 페이지를 보여준 이후에 뒤늦게 어떤 HTML 정보가 추가되거나 삭제되어 마치 화면이 덜컥 거리는 것과 같은 부정적인 사용자 경험을 말함

사용자의 디바이스 성능에 비교적 자유로움

자바스크립트 리소스 실행은 사용자의 디바이스에서만 실행되어 사용자 디바이스 성능에 의존적임. 서버 사이드 렌더링을 수행하면 이런 부담을 서버가 덜어감.

보안에 좀 더 안전

API 호출과 인증 같이 사용자에게 노출되면 안되는 민감한 작업을 서버에서 수행하고 그 결과만 브라우저에게 제공해 보안위협을 피할 수 있음

서버 사이드 렌더링의 단점

소스코드 작성 시 서버 고려해야 함

서버사이드에서 작성하는 코드는 브라우저 전역 객체인 window, sessionStorage 사용에 제한이 있음

서버 리소스 준비되어야함

SPA는 단순히 HTML, js, css 리소스를 다운로드할 수 있는 준비만 하면됨. 서버 사이드 렌더링은 사용자의 요청을 받아 렌더링을 수행할 서버가 필요.

React에서도 서버 사이드 렌더링을 위한 API를 제공한다

renderToString

function ChildrenComponent({ fruits }) {
	useEffect(() => {
		cosnole.log(fruits);
	}
	
	return (
		<ul>
			{fruits.map((fruit) => (
				<li key={fruit}>
					{fruit}
				</li>
			))}
		</ul>
	)
}

function SampleComponent() {
	return (
		<>
			<div>hello</div>
			<ChildrenComponent fruits={['apple', 'banana', 'peach']} />
		</>
	)
}

const result = ReactDOMServer.renderToString(
	React.createElement('div', { id: 'root' }, <SampleComponent />),
)

renderToString 을 사용해서 실제 브라우저가 그려야 할 HTML 결과를 만들어낸 모습이 아래와 같다.

<div id='root' data-reactroot=''>
	<div>hello</div>
	<ul>
		<li>apple</li>
		<li>banana</li>
		<li>peach</li>
	</ul>
</div>

눈여겨 볼 만한것은 useEffect 같은 클라이언트 사이드에서 실행되는 훅은 실행되지 않는다.

renderToStaticMarkup

renderToString과 매우 유사하지만 루트 요소에 추가했던 data-reactroot와 같은 리액트에서만 사용하는 추가적인 DOM 속성을 만들지 않는다. 리액트에서만 사용하는 속성을 제거하면 결과물인 HTML의 크기를 아주 약간이라도 줄일 수 있는 장점이 있음

renderToStaticMarkup은 리액트의 이벤트 리스너가 필요 없는 완전히 순수한 HTML을 만들때에만 사용함

renderToNodeStream

renderToString과 결과물이 완전히 동일하지만, HTML의 결과물이 매우 큰 경우 사용한다. 스트림을 활용하여 큰 크기의 데이터를 청크 단위로 분리해 순차적으로 처리할 수 있음

renderToStaticNodeStream

renderToNodeStream에서 순수한 HTML 결과물이 필요할 때 사용

프로덕션 프로젝트에서 React가 제공하는 서버 사이드 렌더링 API 사용을 권하지 않는다

리액트 팀 또한 리액트 서버 사이드 렌더링을 직접 구현해 사용하는 것보다는 리액트 팀과 긴밀하게 협조하고 있는 Next.js 같은 프레임워크를 사용하는 것을 권장하고 있음

Next.js

/pages/_app.tsx

  • 에러 바운더리를 사용해 애플리케이션 전역에서 발생하는 에러 처리
  • reset.css 같은 전역 CSS 선언
  • 모든 페이지에 공통으로 사용 또는 제공해야 하는 데이터 제공 등

/pages/_document.tsx

  • 애플리케이션의 HTML을 초기화하는 곳
  • 웹사이트의 뼈대가 되는 HTML 설정과 관련된 코드를 추가하는 곳
  • 반드시 서버에서 렌더링 됨

getStaticPaths와 getStaticProps

  • 블로그, 게시판, 약관 같이 정적으로 결정된 페이지를 보여주고자 할 때 사용
  • 정적으로 제공해야할 페이지 수가 적다면 페이지를 빌드 시점에 미리 준비해 두거나 혹은 fallback을 사용해 사용자의 요청이 있을 때만 빌드하는 등의 최적화를 추가할 수 있음

getServerSideProps

  • getServerSideProps의 props로 내려줄 수 있는 값은 JSON으로 제공할 수 있는 값으로 제한, class나 Date 등은 props로 제공할 수 없음

  • API 호출 시 /api/some/path와 같이 protocol과 domain 없이 fetch 요청을 할 수 없음, 브라우저와 다르게 서버는 자신의 호스트를 유추할 수 없기 때문

  • 어떤 조건에 따라 다른 페이지로 보내고 싶다면 redirect를 사용할 수 있음

    → 해당 페이지를 보여주기도 전에 원하는 페이지로 보내버릴 수 있어 사용자에게 훨씬 더 자연스럽게 보여줄 수 있음!

export const getServerSideProps: GetServerSideProps = async (context) => {
	const { query: { id = '' },} = context
	const post = await fetchPost(id.toString())
	
	if (!post) {
		redirect: {
			destination: '/404'
		}
	}
	
	return {
		props: { post },
	}
}

Next.config.js

  • basePath: 개발 환경으로 치면 localhost:3000/이 접근 가능한 주소가 되는데, 여기에 basePath: “docs”와 같이 문자열을 추가하면 localhost:3000/docs 에 서비스가 시작됨. 클라이언트 렌더링을 트리거 하는 모든 주소에 알아서 basePath가 붙은 채로 렌더링 및 작동함
  • poweredByHeader: Next.js는 응답 헤더에 X-Power-by: Next.js 정보를 제공하는데, false를 선언하면 이 정보가 사라짐. 기본적으로 보안 관련 솔루션에서는 powered-by 헤더를 취약점으로 분류하므로 false로 설정하자

5장: 리액트와 상태관리 라이브러리

Flux 패턴의 등장

뷰(HTML)가 모델(javascript)를 변경할 수 있으며, 반대의 경우 모델도 뷰를 변경할 수 있음. 이는 코드 양이 많아지고 시나리오가 복잡해지면 관리가 매우 어려워짐. 페이스북 팀은 양방향이 아닌 단방향으로 데이터 흐름을 변경하는것을 제안 → Flux 패턴의 시작

  • 액션: 어떠한 작업을 처리할 액션과 그 액션 발생 시 함께 포함시킬 데이터 의미, 액션 타입과 데이터를 각각 정의해 디스패처로 보냄
  • 디스패처: 액션을 스토어로 보내는 역할
  • 스토어: 상태에 따른 값과 변경할 수 있는 메서드를 갖고 있음
  • 뷰: 리액트 컴포넌트

Redux

  • Flux 구조를 구현하기 위해 만들어진 라이브러리
  • Elm 아키텍처를 도입
    • Elm: 웹페이지를 선언적으로 작성하기 위한 언어

리덕스는 하나의 상태 객체를 스토어에 저장해두고, 이 객체를 업데이트하는 작업을 디스패치해 업데이트를 수행함 → reducer로 트리거

단점

하나의 상태를 바꾸고 싶어도 해야할 일이 너무 많음 → 보일러 플레이트 부담

Context API, useContext

  • 리액트 16.3에서 전역 상태를 하위 컴포넌트에 주입할 수 있는 Context API 출시
  • props로 상태를 넘겨주지 않더라도 Context API를 사용하면 원하는 곳에서 Context Provider가 주입하는 상-태를 사용
  • Context API는 상태 관리가 아닌 주입을 도와주는 기능이며, 렌더링을 막아주는 기능 또한 존재하지 않으므로 주의가 필요

Recoil

  • 리액트에서 훅의 개념으로 상태관리를 시작한 최초의 라이브러리
  • 비교적 오랜 시간이 흘렀음에도 아직 정식으로 출시한 라이브러리가 아님 (1.0.0 버전이 나오지 않음)

RecoilRoot

  • 왜 최상위 컴포넌트에 선언? Recoil에서 생성되는 상태값을 저장하기 위한 스토어를 생성
  • 스토어의 상태값에 접근할 수 있는 함수들이 있으며, 이 함수를 활용해 상태값에 접근, 변경
  • 값의 변경이 발생하면 이를 참조하고 있는 하위 컴포넌트에게 모두 알림

atom

  • key값은 앱 내의 유일한 값이어야함 → atom, selector를 만들 때 주의

6장: 리액트 개발 도구로 디버깅하기

리액트 개발 도구 활용

  • 익명 함수로 선언하기 곤란한 경우, 혹은 함수명과는 별도로 특별한 명칭을 부여해 명시적으로 확인이 필요한 경우에 displayName을 사용하자
const MemoizedComponent = memo(function() {
	return <>MemoizedComponent</>
})

MemoizedComponent.displayName = "메모 컴포넌트"
  • 고차 컴포넌트의 경우 displayName을 유용하게 사용할 수 있음 (일반적으로 일반 컴포넌트의 조합으로 구성되므로)
  • 프로파일러 > Flamegraph 탭을 사용하면 렌더 커밋별로 어떠한 작업이 일어났는지를 알 수 있는데, 개발자가 의도한 대로 메모이제이션이 작동하고 있는지 혹은 상태변화에 따라서 렌더링이 의도한 대로 제한적으로 발생하고 있는지 확인할 수 있음
  • Flamgraph의 오른쪽에 있는 화살표를 누르거나 세로 막대 그래프를 클릭하면 각 렌더 커밋별로 리액트 트리에서 발생한 렌더링 정보를 확인할 수 있음

7장: 크롬 개발자 도구를 활용한 애플리케이션 분석

크롬 개발자 도구에서 웹사이트를 제대로 디버깅하고 싶다면 시크릿 모드 또는 프라이빗 모드라 불리는 개인정보 보호 모드에서 페이지와 개발자 도구를 여는 것을 권장

→ 브라우저에 설치돼 있는 각종 확장프로그램이 전역 변수나 HTML 요소에 추가할 수 있음

메모리 탭

메모리 탭을 열면 리액트 개발 도구의 프로파일과 비슷하게 프로파일링 작업을 거쳐야 원하는 정보를 볼 수 있음

프로파일링 유형

  1. 힙 스냅샷: 현재 메모리 상황을 사진 찍듯이 촬영 할 수 있음. 현재 시점의 메모리 상황을 알고 싶을 때 사용
  2. 타임라인의 할당 계측: 현재 시점의 메모리 상황이 아닌, 시간의 흐름에 따라 메모리의 변화를 살펴보고 싶을 때 사용. 주로 로딩이 되는 과정의 메모리 변화 또는 페이지에서 어떠한 상호작용을 했을 때 메모리의 변화 과정을 알고 싶을 때 사용.
  3. 할당 샘플링: 메모리 공간을 차지하고 있는 자바스크립트 함수를 볼 수 있음

자바스크립트 인스턴스 VM 선택

프로파일링에 앞서 자바스크립트 인스턴스 VM을 선택함. 환경별 힙 크기를 볼 수 있는데, 실제 해당 페이지가 자바스크립트 힙을 얼마나 점유하고 있는지 나타냄. 이 크기만큼 브라우저에 부담을 줌

얕은 크기와 유지된 크기의 차이점

얕은 크기: 객체 자체가 보유하는 메모리 바이트의 크기

유지된 크기: 해당 객체 자체 뿐만 아니라 다른 부모가 존재하지 않는 모든 자식 객체들의 크기까지 더한 값

메모리 누수를 찾을 때는 얕은 크기는 작으나 유지된 크기가 큰 객체를 찾아야 함

→ 두 크기의 차이가 큰 객체는 다수의 다른 객체를 참조하고 있다는 뜻

→ 이는 해당 객체가 복잡한 참조 관계를 가지고 있다는 뜻

8장: 좋은 리액트 코드 작성을 위한 환경 구축하기

ESLint

eslint-plugin

  • eslint-plugin으로 시작하는 플러그인은 규칙을 모아놓은 패키지
  • 예를들어, eslint-plugin-import 라는 패키지는 자바스크립트에서 다른 모듈을 불러오는 import와 관련된 다양한 규칙을 제공

eslint-config

  • eslint-plugin을 한데 묶어서 완벽하게 한 세트로 제공하는 패키지

eslint-config-airbnb

  • 가장 먼저 손에 꼽는 eslint-config
  • 에어비앤비 개발자 뿐만 아니라 500여 명의 수많은 개발자가 유지보수하고 있는 유명한 eslint-config

eslint-config-next

  • 리액트 기반 Next.js 프레임워크를 사용하고 있는 프로젝트에서 사용할 수 있는 eslint-config
  • 단순히 자바스크립트 코드를 정적으로 분석할 뿐만 아니라 페이지나 컴포넌트에서 반환하는 JSX 구문 및 _app, _document에서 작성돼 있는 HTML 코드 또한 정적 분석 대상으로 분류해 제공함
  • next.js를 사용한다면 반드시 설치

import react

리액트 17 버전 이상을 사용하고 있따면 import React 구문을 모두 확인한 후에 제거하자

규칙에 대한 예외처리

만약 일부 코드에서 특정 규칙을 임시로 제외시키고 싶다면 eslint-disable- 주석을 사용하자

// 특정 줄만 제외
console.log('hello world') // eslint-disable-line no-console

// 다음 줄 제외
// eslint-disable-next-line no-console
console.log('hello world')

// 특정 여러 줄 제외
/* eslint-disable no-console */
console.log('a')
console.log('b')
/* eslint-enable no-console */

// 파일 전체에서 제외
/* eslint-disable no-console */
console.log('c')

테스트

테스트의 목적

  • 내가 코딩을 한 의도대로 작동하는지 확인
  • 버그를 사전에 방지
  • 잘못된 작동으로 인해 발생하는 비용 줄이기

데이터셋

HTML의 특정 요소와 관련된 임의 정보를 추가할 수 있는 HTML 속성, HTML의 특정 요소에 data-로 시작하는 속성은 무엇이든 사용할 수 있음

비동기 이벤트가 발생하는 컴포넌트

jest.spyOn(window, 'fetch').mockImplementation(
	jest.fn(() => 
		Promise.resolve({
			ok: true,
			status: 200,
			json: () => Promise.resolve(MOCK_TODO_RESPONSE),
		}),
	) as jest.Mock,
)

위 코드는 모든 시나리오(서버 오류 등)를 테스트 할 수 없으므로 (일일이 모킹해야함), MSW를 사용해야함

  • MSW(Mock Service Worker): 서비스 워커를 활용해 실제 네트워크 요청을 가로채는 방식으로 모킹을 구현하는것
    • fetch 요청을 하는 것과 동일하게 네트워크 요청을 수행하고, 이 요청을 중간에 MSW가 감지하고 미리 준비한 모킹 데이터를 제공하는 방식
import { rest } from 'msw'
import { setupServer } from 'msw/node'

// 응답을 모킹
const MOCK_TODO_RESPONSE = {
	userId: 1,
	id: 1,
	completed: false,
}

// 서버 생성
const server = setupServer(
	rest.get('/todos', (req, res, ctx) => {
		const todoId = req.params.id
		return res(ctx.json({ ...MOCK_TODO_RESPONSE, id: Number(todoId) }))
	}
)

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

beforeEach(() => {
	render(<FetchComponent />)
})

테스트 코드 작성하기 전

  • 최우선 과제는 애플리케이션에서 가장 취약하거나 중요한 부분을 파악하는 것
  • 사용자의 작업과 최대한 유사하게 작성돼야 함

9장: 모던 리액트 개발 도구로 개발 및 배포 환경 구축하기

tsconfig.json 작성

{
	"$schema" : "https://json.schemastore.org/tsconfig.json"
}

tsconfig.json을 작성하기 전에 위와 같이 JSON 최상단에 $schema키와 해당하는 값을 넣어주면 IDE에서 자동 완성이 가능해짐

이 밖에도 .eslintrc, .prettierrc와 같이 JSON 방식으로 설정하는 라이브러리가 schemastore에 해당 내용을 제공하고 있다면 더 편리하게 JSON 설정을 작성할 수 있음

옵션

  • compilerOptions: 타입스크립트 → 자바스크립트 컴파일 할 때 사용
    • target: 타입스크립트가 목표로 하는 언어의 버전, 폴리필까지 지원 X
    • allowJs: 타입스크립트가 자바스크립트 파일 또한 컴파일할지 결정 (.js, .ts 혼재 되어 있을 경우 사용)
    • skipLibCheck: d.ts에 대한 검사여부를 결정, 컴파일 시간이 길어지므로 일반적으로 꺼놓음
    • forceConsistentCasingInFileNames: 파일 이름의 대소문자를 구분하도록 강제
    • esModuleInterop: CommonJS 방식으로 보낸 모듈을 ES 모듈 방식의 import로 가져올 수 있게 함
    • module: commonjs와 esnext가 대표적으로 있는데, commonjs는 require, esnext는 import를 사용하므로 import를 사용할 수 있는 환경에서는 esnext를 사용하는 것이 좋음
    • moduleResolution: node는 node_modules를 기준으로 모듈을 해석하고 classic은 tsconfig.json이 있는 디렉터리를 기준으로 모듈을 해석함. node는 module이 commonJS일 때만 사용가능
    • resolveJsonModule: JSON 파일을 import할 수 있게 해줌 → 자동으로 allowJS 켜짐
    • incremental: 활성화되면 타입스크립트는 마지막 컴파일 정보를 .tsbuildinfo 파일 형태로 만들어 디스크에 저장함 → 컴파일이 더 빨라지는 효과
    • baseUrl: 모듈을 찾을 때 기준이 되는 디렉터리 지정
    • path: 일반적으로 모듈을 불러오게 되면 상대 경로를 활용하게 되는데 path를 활용해서 alias를 지정할 수 있음
      • @의 사용은 자제하자, 보통 @angular, @types와 같이 스코프 패키지에 널리 사용되기 때문
      • “#pages/” : [”pages/”] 이런식으로 사용함
  • include: 컴파일 대상에서 포함시킬 파일 목록
  • exclude: 컴파일 대상에서 제외시킬 파일 목록

next.config.js 작성하기

옵션

  • reactStrictMode: 리액트의 엄격 모드 활성화
  • poweredByHeader: 일반적으로 보안 취약점으로 언급되는 X-Powered-By 헤더 제거
  • eslint.ignoreDuringBuilds: 빌드 시에 ESLint를 무시, ESLint는 CI 과정에서 별도로 작동핳게 만들어 빌드를 더욱 빠르게 만들 것

일반적인 파일 구조

  • pages
  • components
  • hooks
  • types
  • utils

파일 구조에 정답은 없고, path alias를 적용한다면 코드 내에서의 가독성 확보

템플릿

요즘 대다수의 서비스가 마이크로 프론트엔드를 지향하기 때문에 프로젝트를 구축할 일이 잦아질 것

→ 템플릿 사용

먼저, 보일러 플레이트 프로젝트를 만든 다음, 깃허브에서 ‘Template repository’ 옵션을 체크해두자

다른 저장소를 생성할 때 이 템플릿을 사용할 수 있음

깃허브 활용하기

깃허브 액션

  • 러너: 깃허브 액션이 실행되는 서버, 지정하지 않으면 공용 깃허브 액션 서버를 이용
  • 액션: 러너에서 실행되는 하나의 작업 단위, yaml 파일로 작성된 내용을 하나의 액션
  • 이벤트: 액션의 실행을 일으키는 이벤트를 의미
    • pull_request: PR이 열리거나, 닫히거나, 수정되거나, 할당되거나, 리뷰 요청 등
    • issues
    • push: 커밋이나 태그가 푸시될 때
    • schedule: 특정 시간에 실행되는 이벤트 (cron)
  • 잡(jobs): 하나의 러너에서 실행되는 여러 스텝의 모음을 의미 → 병렬로 실행
  • 스텝(steps): 잡 내부에서 일어나는 하나하나의 작업 → 병렬로 일어나지 않음

브랜치 보호 규칙

머지하기 전에 꼭 성공해야 하는 액션이 있다면 저장소에 브랜치 보호 규칙을 추가할 수 있음

특히, build를 실패했을 때 머지를 하지 못하게 설정할 수 있음 (build.yaml 파일 작성)

Dependabot

  • 취약점을 Critical, High, Moderate, Low의 4단계로 분류
  • 열어준 PR을 검토해서 머지가 가능한지 살펴보자
  • 가능한 의존성을 최소한으로 유지하자
  • Dependabot이 경고하는 문제에 대해 계속 관심가지자
  • 절대 완벽하게 수정해준다고 맹신하진 말자 (무작정 머지 금지)
  • Dependabot으로 수정하기 어려운 이슈라면 npm의 overrides를 적극 활용해보자

배포

빠르게 배포할 수 있는 서비스

  1. Vercel
  2. Netlify
  3. DigitalOcean

10장: 리액트 17과 18의 변경사항 살펴보기

17 버전 살펴보기

리액트 17버전은 16버전과 다르게 새롭게 추가된 기능이 없으며 호환성이 깨지는 변경 사항, 기존에 사용하던 코드의 수정을 피룡로 하는 변경 사항을 최소화했음

따라서 16 → 17 버전의 업그레이드는 대부분 순조롭게 함

이벤트 위임 방식 변경

16 버전에서는 모든 이벤트는 document에 부착되었는데, 17 버전에서는 document가 아니라 리액트 최상단 요소에 추가 됨

import React from ‘react’가 필요없음

리액트 17부터 바벨과 협력해 import 구문 없이도 JSX를 변환할 수 있게 됨

→ 불필요한 import 구문을 삭제해 번들링 크기를 약간 줄임

useEffct 클린업 함수의 비동기 실행

16 버전까지는 동기적으로 실행돼 클린업 함수가 완료되기 전까지는 다른 작업을 방해하므로 불필요한 성능 저하로 이어졌었음

→ 17 버전에서는 화면이 완전히 업데이트된 이후에 클린업 함수가 비동적으로 실행, 즉 화면이 업데이트가 완전히 끝난 이후에 실행되도록 바뀜

18 버전 살펴보기

리액트 18에서는 리액트 17에서 하지 못했던 다양한 기능들이 추가됨

새로 추가된 훅

  1. useId

useId는 컴포넌트별로 유니크한 값을 생성하는 새로운 훅

서버사이드와 클라이언트 간에 동일한 값이 생성되어 하이드레이션 이슈도 발생하지 않음

  1. useTransition

상태 변경으로 인해 무거운 작업이 발생하고, 이로 인해 렌더링이 가로막힐 여지가 있는 경우 사용

export default function TabContainer() {
	const [isPending, startTransition] = useTransition()
	const [tab, setTab] = useState<Tab>('about')
	
	function selectTab(nextTab: Tab){
		startTransition(() => {
			setTab(nextTab)
		})
	}
	
	return (
		<>
			{isPending ? ('로딩중') : (<> {tab === 'about' && <About />}{ tab === 'post' && <Post />} </>)
		</>
	)
}
  • useTransition은 아무것도 인수로 받지 않고, isPending과 startTransition이 담긴 배열을 반환
  • isPending은 상태 업데이트가 진행중인지 확인할 수 있는 boolean
  • startTransition은 긴급하지 않은 상태 업데이트로 간주할 set 함수를 넣어둘 수 있는 함수를 인수로 받음
  • Post가 무거운 작업일 경우 Post를 클릭했을 때 로딩 중이라는 메시지와 함께 렌더링이 시작되며 이후에 바로 About 탭으로 이동하면 그 즉시 Post 렌더링이 중단되고 About 렌더링을 시작
  • useTransition과 같은 동시성을 지원하는 기능을 사용하면 느린 렌더링 과정에서 로딩 화면을 보여주거나 혹은 지금 진행 중인 렌더링을 버리고 새로운 상태값으로 다시 렌더링하는 등의 작업을 할 수 있음
  1. useDeferredValue
  • 리액트 컴포넌트 트리에서 리렌더링이 급하지 않은 부분을 지연할 수 있게 도와주는 훅임
  • 특정 시간동안 발생하는 이벤트를 하나로 인식해 한 번만 실행하게 해주는 디바운스와 비슷하지만 디바운스는 고정된 지연 시간을 필요로 하지만 useDeferredValue는 고정된 지연 시간 없이 첫번째 렌더링이 완료된 이후에 이 value로 지연된 렌더링을 수행
  1. useInsertionEffect
    DOM 변경 작업 이전에 실행되는 훅, 실제로 사용할 일 없고 권고하지 않음
profile
Frontend Developer

1개의 댓글

comment-user-thumbnail
2025년 1월 17일

멋진 리뷰

답글 달기