kimyenac
techreview
react
《모던 리액트 Deep Dive》 책 리뷰/정리
2024-10-23

📍Description

모던 리액트 Deep Dive 책을 읽으며 정리한 내용을 공유합니다. 책 링크






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


훅 구현체 찾아가기

  • 코어는 다른 패키지의 기능을 개발자에게 제공해 줄 때 의존성을 자기가 만들지 않고 외부에서 주입 받음. 더 나아가 리액트는 외부에서 의존성을 주입할 때 코어에 직접 주입하지 않고, 중간자를 하나 더 두게 되는데 코어에서는 ReactSharedInternals.js가 이에 해당하고 리액트 프로젝트 전체로 보면 shared라는 패키지가 이 역할을 함


JSX

  • JSX 는 HTML 이나 XML 을 자바스크립트 내부에 표현하는 것이 유일한 목적은 아니다. JSX 내부에 트리 구조로 표현하고 싶은 다양한 것들을 작성해두고, 이 JSX를 트랜스파일 과정을 거쳐 자바스크립트가 이해할 수 있는 코드로 변경하는 것이 JSX 의 목표라고 할 수 있다.
  • JSX 구성 컴포넌트
    • JSXElement : HTML 요소와 비슷한 역할을 하는 가장 기본 요소
    • JSXAttributes : JSXElement에 부여할 수 있는 속성 (필수 아님)
    • JSXChildren : JSXElement의 자식 값을 나타냄 (0개 이상 존재 가능)
    • JSXStrings : 문자열 (개발자가 HTML의 내용을 손쉽게 JSX 로 가져올 수 있도록 의도적으로 설계된 부분)
  • JSX 문법에는 있지만 실제로 리액트에서 사용하지 않는 것들
    • JSXNamespacedName
    • JSXMemberExpression


가상 DOM과 리액트 파이버

  • 가상 DOM 을 통한 DOM 관리가 직접 DOM 을 조작하는 것보다 빠르다고 오해를 하는데, 가상 DOM 의 diffing, 배치 업데이트 과정에 추가적인 리소스 소모가 있을 수 있다. 실제로 직접 DOM을 조작해서 리액트보다 더 빠른 속도를 가진 라이브러리들이 존재함. (svelte 🙃)
    • 가상 DOM 의 이점은 상태 변경에 따라 전체 UI를 새로 그리는 것처럼 개발할 수 있으나 실제 DOM 반영은 일부만 되는 것과, 컴포넌트 트리를 가상 DOM 이란 레이어로 추상화하여 실제로 그리는 렌더러를 바꿀 수 있는 것.
  • 리액트 파이버 : 리액트에서 어떤 부분을 새롭게 렌더링해야 하는지 가상 DOM과 실제 DOM을 비교하는 작업을 하는 자바스크립트 객체로 하는 일은 다음과 같다.
    • 작업을 작은 단위로 분할하고 쪼갠 다음, 우선순위를 매긴다
    • 이러한 작업을 일시 중지하고 나중에 다시 시작할 수 있다
    • 이전에 했던 작업을 다시 재사용하거나 필요하지 않은 경우에는 폐기할 수 있다
  • 리액트 파이버 트리 : 현재의 모습을 담은 current 파이버 트리와 작업중인 상태를 나타내는 workInProgress 트리가 있음. 리액트 파이버의 작업이 끝나면 트리를 가리키는 포인터만 변경해 workInProgress 트리를 current 트리로 바꿔치기 하는데 이 기법을 더블 버퍼링이라고 함. (리액트에서 더블 버퍼링은 커밋 단계에서 수행됨.)
  • 파이버 동작 방식
    • setState 등으로 업데이트가 발생하면 workInProgress 트리를 빌드하여 상태가 변경되는 부분을 반영 (렌더 단계)
    • 이를 리액트 DOM 혹은 React Native 내부의 렌더러 등이 실제 렌더링을 진행 (커밋 단계)
  • 리액트 파이버와 DOM/Native 렌더링은 별개. 따라서 렌더러가 다르다 하더라도 동일한 재조정자를 사용할 수 있음


클래스 컴포넌트와 함수 컴포넌트

  • 리액트에서는 클래스와 함수를 기반한 컴포넌트를 구현할 수 있는데 클래스 컴포넌트의 한계를 극복하여 장점으로 부각되는 게 함수 컴포넌트. 다만 생명주기 메서드 중 함수 컴포넌트로 모사 불가능한 것이 필요할 경우 클래스 컴포넌트가 필요함.
  • 클래스 컴포넌트의 한계 → 함수 컴포넌트의 장점
    • 데이터 흐름을 추적하기 어려움 → this 와 관련된 혼란을 줄이고 hooks api 를 통해 일관된 흐름을 유지
    • 로직 재사용이 어려움 → 커스텀 훅으로 로직 재사용 가능
    • 클래스의 한계로 번들 크기가 증가됨 → 사용하지 않는 함수는 제거해서 불필요한 번들 크기를 줄일 수 있음
    • 인스턴스 내부에 state 를 관리하여 핫 리로딩에 불리함 → 리액트 아키텍처 내부 클로저에 state 저장하여 핫 리로딩에 최적화 되어있음.
  • 클래스 컴포넌트 vs 함수 컴포넌트
    • 클래스 컴포넌트는 props, state 의 값을 항상 this 로부터 가져옴
    • 함수 컴포넌트는 props 를 함수의 인자로 받는데 컴포넌트가 그 값을 변경할 수 없고, 해당 값을 그대로 사용하게 됨


렌더링은 어떻게 일어나는가?

  • 리액트의 렌더링은 리액트 애플리케이션 트리 안에 있는 모든 컴포넌트들이 현재 가지고 있는 props 와 state 의 값을 기반으로 어떻게 UI를 구성하고 이를 바탕으로 어떤 DOM 결과를 브라우저에 제공할 것인지 계산하는 과정
  • 렌더링이 발생하는 시나리오
    • 최초 렌더링: 처음 애플리케이션 진입 시 발생되는 렌더링으로 최초 화면 그리기 정보 제공을 위함
    • 리렌더링: 최초 렌더링 이후로 발생하는 모든 렌더링을 의미하며, state 업데이트 / key props 변경 등의 이유로 발생됨
  • 렌더링 프로세스 : 상태 변경을 감지하고, 변경된 상태를 기반으로 새로운 가상 DOM 을 생성하여 실제 DOM 에 변경된 결과물을 반영.
  • 렌더와 커밋 단계
    • 렌더 단계 : 상태 변경을 감지해서 변경이 필요한 컴포넌트를 체크 후 이를 기반하여 실제 DOM 에 변경된 결과물을 반영함 (여기서 비교하는 것은 크게 type, props, key)
    • 커밋 단계 : 렌더 단계의 변경 사항을 실제 DOM 에 적용해 사용자에게 보여주는 단계로 변경사항을 계산했는데 아무런 변경사항이 감지되지 않는다면 커밋 단계는 생략될 수 있음. (리액트의 렌더링이 일어난다고 해서 무조건 DOM 업데이트가 일어나는 것은 아니라는 것)


메모이제이션

  • 메모이제이션을 성능 최적화를 위한 기술이지만, 값을 비교하고 렌더링 또는 재계산이 필요한 지 확인하는 작업과 이전에 결과물을 저장해 두었다가 다시 꺼내와야 하는 두 가지 비용이 들기 때문에 두 가지 관점에서 갑론을박이 벌어지고 있음.
    • 주장1 : 섣부른 최적화는 독이다, 꼭 필요한 곳에만 메모이제이션을 추가하자. - 메모이제이션은 값을 어딘가에 저장해두어야 하기 때문에 리소스가 추가로 들고, 성능에 영향이 미미한 작업까지 메모이제이션을 하며 섣부른 최적화는 필요 없다.
    • 주장2 : 렌더링 과정의 비용은 비싸다, 모조리 메모이제이션해 버리자. - 메모이제이션 처리를 하지 않았을 때 치러야 할 잠재적인 위험 비용이 더 크기 때문에 모두 메모이제이션 처리하는 게 더 큰 이점이다.


💭 의견

  • 훅이 등장하면서 함수형 컴포넌트가 득세하게 된 리액트 16.8 이후에 개발을 시작하였기에 클래스 컴포넌트에 대한 지식이 많이 부족하다는 것을 느끼고 있다. 물론 함수형 컴포넌트가 많아졌지만 책에서 말하는 것처럼 현재까지도 클래스로 구현된 컴포넌트들이 많고. (자식 컴포넌트에서 발생한 에러에 대한 처리는 현재 클래스 컴포넌트로만 가능하므로) 에러 처리를 위해서라도 클래스 컴포넌트에 대한 지식은 어느 정도 필요하다고 판단해서 클래스 컴포넌트 공부에 대한 필요성을 느꼈다.
  • 무거운 연산으로 인한 성능 문제를 해결할 수 있는 메모이제이션 처리는 꼭 필요한 기능이지만 불필요한 메모이제이션 처리는 최적화와 개발자 성장에 악영향을 끼친다고 생각한다. 누군가 내 코드를 보고 ‘왜 이렇게 구현했어요?’ 혹은 ‘왜 이걸 썼어요?’ 했을 때 선뜻 대답이 나오지 않는다면 안 쓰는 게 맞다고 생각하는데, ‘무조건 메모이제이션 처리하는 게 성능 상 이점이 있을 것 같아서요’ 는 너무 무책임한 말이지 않은가….. 💭 이슈를 진행하는데 ‘이거 무거워 보이는데?’ 싶어서 테스트 없이 메모이제이션 처리를 하게 되었다면 나중에 리팩토리할 때 꼭 테스트를 진행해보는 게 좋겠다!


❔책을 읽으며 궁금했던 것들

  • 🙋🏻‍♀️ JSXElementName > JSXNameSpacedName 은 뭐지?
    • JSXIdentifier:JSXIdentifier의 조합으로 ':' 을 통해 서로 다른 식별자를 이어주는 문법으로 두 개 이상은 불가능
      • JSXIdentifier 는 JSX 내부에서 사용하는 식별자
    • React 에선 사용하지 않는 문법
  • DOM을 방문할 때 display:none과 같이 눈에 보이지 않는 요소는 스킵한다고 했는데
    • 🙋🏻‍♀️ CSSOM은 순회하지 않고 DOM만 방문하는데 해당 정보는 어떻게 하는걸까?
      • 브라우저는 DOM과 CSSOM을 결합하여 렌더 트리를 생성한다. 이 과정에서 display: none과 같은 스타일이 적용된 요소는 렌더 트리에 포함되지 않음. 따라서 브라우저는 DOM을 순회하면서 CSSOM의 정보를 참조하여 렌더 트리를 구성하게 되는 것.
    • 🙋🏻‍♀️ 스타일시트 파싱이 HTML 파싱보다 먼저 일어나는건가?
      • 일반적으로 HTML 파싱과 CSS 파싱은 병렬로 발생. 그러나 CSS 파일이 로드되기 전까지는 렌더링이 차단될 수 있음. 이는 CSS가 렌더 트리에 영향을 미치기 때문.
    • 🙋🏻‍♀️ stylesheet말고 인라인으로 작성하면?
      • 인라인 스타일을 사용해도 display: none이 적용된 요소는 렌더 트리에 포함되지 않으며, 이는 외부 스타일시트와 동일한 동작을 함
    • 🙋🏻‍♀️ 리액트 파이버에서 작업단위를 나누고 우선순위를 매길 때 일시중지하고 재사용 폐기를 하는 그 기준이 뭘까?
      • 아래 참고


📑 리액트 파이버에서 처리하는 우선순위의 기준

  • ✔️ 우선순위를 나누는 기준 값 : expirationTime
    • expirationTime 은 fiber 객체 내부에 있으며, 사용자가 이벤트를 발생시켰을 때의 시점값을 나타냄.
    • expirationTime 이 클수록 먼저 발생한 업데이트라는 것이므로 먼저 처리해야 되는 업데이트로 판단함.
    • expirationTime 이 Sync (동기적으로 처리해야 하는 이벤트) 인 경우 가장 높은 우선순위를 주게 되고, Never 나 Idle (유휴상태) 인 겨우, 낮은 우선순위를 주게 되는데
    • 처리 우선순위가 높은 순서는 “Immediate > UserBlocking > Normal > Idle” 여기서 위를 판단하는 기준이 되는 게 expirationTime
  • ✔️ 기타 참고
    • Legacy mode 에서는 대부분 expirationTime 이 Sync
    • 핵심은 업데이트 유형에 따라 우선 순위가 다르므로 애니메이션 업데이트는 데이터 저장소의 업데이트보다 더 빨리 완료되어야 함.
    • pendingWorkPriority 는 보류 중인 작업 우선순위 파이버가 나타내는 작업의 우선순위를 나타내는 숫자
    • requestAnimationFrame() 는 높은 우선 순위 함수 스케줄링
    • requestIdleCallback() 는 낮은 우선 순위 함수 스케줄링





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


리액트의 모든 훅 파헤치기

  • useState : 함수 컴포넌트 내부에서 상태를 정의하고, 이 상태를 관리할 수 있게 해주는 훅
    • 클로저를 활용하여 구현되었는데, 어떤 함수 (useState) 내부에 선언된 함수 (setState) 가 실행이 종료된 이후에도 (== useState 가 종료된 이후) 지역 변수인 state 를 계속 참조할 수 있음
    • 게으른 초기화 : useState 의 인수를 변수가 아닌 함수로 넘겨주는 경우를 의미하는데 오로지 state 가 처음 만들어질 때만 사용되고, 이후 리렌더링이 발생해도 이 함수의 실행은 무시된다. 따라서 무거운 연산을 포함해 실행 비용이 많이 드는 경우 사용하는 것이 좋음
  • useEffect : 애플리케이션 내 컴포넌트의 여러 값을 활용해 동기적으로 부수 효과를 만드는 메커니즘
    • 의존성 배열에 빈 배열 선언 시 비교할 의존성이 없다고 판단해 최초 렌더링 직후에 실행된 다음 더 이상 실행되지 않고, 아무런 값도 넘겨주지 않는다면 의존성을 비교할 필요가 없어 렌더링 발생할 때마다 실행된다
  • useMemo : 비용이 큰 연산에 대한 결과를 저장(메모이제이션)하고 반환하는 훅
    • 컴포넌트도 감싸서 사용할 수 있는데 컴포넌트는 보통 React.memo 를 사용
  • useCallback : useMemo 가 값을 기억했다면, useCallback 은 함수 자체를 기억하는 훅
    • useMemo 로 useCallback 을 구현할 수 있고, 둘 다 메모이제이션을 하는 동일한 기능을 갖고 있는데, useCallback 을 따로 제공하는 이유는 useMemo 로 useCallback 을 구현할 경우 불필요하게 코드가 길어질 수 있기 때문으로 추측된다
  • useRef : 렌더링에 영향을 미치지 않는 고정된 값을 관리하기 위한 훅
    • 값이 변경되어도 렌더링에 영향을 미치지 않는다는 점을 고려하였을 때 useMemo 에 빈배열을 넣는 걸로 useRef 를 구현할 수 있음
  • useContext : props drilling 을 극복하기 위한 Context 를 함수 컴포넌트에서 사용할 수 있게 해주는 훅
    • Provider 에 의존성을 가지고 있기 때문에 아무데서나 재활용할 수 없음. 그리고 내부의 자식 요소들은 모두 리렌더링되기 때문에 context 를 사용하는 환경에서 리렌더링을 막기 위해선 React.memo 를 사용해야 됨.
  • useReducer : useState 의 심화버전 훅
    • useState 와 동일하게 state, dispatcher 2개를 반환한다
    • 2개에서 3개의 인수가 필요한데, 기본 action 을 정의하는 reducer, 초기값을 의미하는 initialState, 초깃값을 지연해서 생성시키고 싶을 때 (게으른 초기화) 사용하는 init 이 있음
    • useReducer 나 useState 둘 다 세부 작동과 쓰임에만 차이가 있을 뿐, 클로저를 활용해 값을 가둬서 state 를 관리한다는 건 동일하다. 관리해야 될 state 값이 복잡하고 이를 수정하는 경우의ㅣ 수가 많아질 경우 useReducer 로 관리하는 게 효율적일 수 있다
  • useImperativeHandle : 부모에게서 넘겨받은 ref 를 원하는대로 수정할 수 있는 훅
  • useLayoutEffect : useEffect 와 비슷하지만, DOM 은 계산됐지만 이것이 화면에 반영되기 전에 하고 싶은 작업이 있을 때 사용되는 훅
    • 실행 순서 : 리액트가 DOM 을 업데이트 → useLayoutEffect 를 실행 → 브라우저에 변경사항을 반영 → useEffect 를 실행
  • useDebugValue : 사용자 정의 훅 내부의 내용에 대한 정보를 남길 수 있는 훅
    • 오직 다른 훅 내부에서만 실행할 수 있고, 컴포넌트 레벨에서 실행한다면 작동하지 않을 것이므로 사용할 때 주의해서 사용하기
    • 공통 훅을 제공하는 라이브러리나 대규모 웹 애플리케이션에서 디버깅 관련 정보를 제공하고 싶을 때 유용하게 사용 가능
  • 훅의 규칙
    • 최상위에서만 훅을 호출해야 하며 반복문이나 조건문, 중첩된 함수 내에서는 훅을 실행할 수 없다. 이 규칙을 따라야만 컴포넌트가 렌더링될 때마다 항상 동일한 순서로 훅이 호출되는 것을 보장할 수 있다
    • 훅을 호출할 수 있는 것은 리액트 함수형 컴포넌트와 사용자 정의 훅에서만 가능하며 일반 자바스크립트 함수에서는 훅을 사용할 수 없다


사용자 정의 훅 vs 고차 컴포넌트

  • 중복 코드를 피하기 위해 재사용 로직을 관리할 수 있는 방법 중 대표적인 게 사용자 정의 훅을 사용하는 것과 고차 컴포넌트를 사용하는 게 있음
  • 단순히 리액트에서 제공하는 훅으로만 공통 로직을 격리할 수 있는 경우, 컴포넌트 전반에 걸쳐 동일한 로직으로 값을 제공하거나 특정한 훅의 작동을 취하게 하고 싶다면 사용자 정의 훅을 사용하는 게 좋음
  • 렌더링의 결과물에도 영향을 미치는 공통 로직이라면 고차 컴포넌트를 사용하는 것이 좋음 (로그인 검증 작업, 에러 바운더리와 같은 상황이 대표 예시)


💭 의견

  • 업무를 진행하면서 useContext 를 사용하고 있는 코드를 수정한 적이 있는데, 그땐 useContext 가 뭐하는 훅인지 정도의 인지만 있고 어떻게 동작되는 지와 장단점은 생각해보지 못했는데 이번 기회에 파보니.. 코드의 복잡성과 렌더링의 문제를 고려할 때 라이브러리 추가에 이슈가 있는 게 아니라면 다른 전역 상태 라이브러리를 사용하는 게.. 💭
  • 보통 코드를 짤 때 리렌더링 시 상태 관리가 필요한 경우 useState 를 무조건 썼던 것 같은데 (관리할 값이 복잡하여도) useReducer 를 활용하면 관리하기 편할 것 같아서 리팩토리 때 참고할 수 있을 것 같다!
  • 책에서 언급된대로 useImperativeHandle 는 생소한 훅이여서 추가로 레퍼런스를 찾아봤는데, 자주 사용하지 말고 상위 컴포넌트에서 하위 컴포넌트의 상태 값을 변경하거나 특정 함수를 참조해야 하는 엣지 케이스에서만 사용하는 것이 올바른 이용법이라는 이야기를 봤다. ref 를 사용할 때 고민하지 않았던 부분들이라 새롭게 다가왔던 것 같은데 ref 의 커스터마이징은 DOM 객체의 함수 실행 등 꼭 필요한 경우에만 사용하고.. 꼭 필요하지 않거나 대체가 가능하다면 수정할 필요가 있을 것 같다. (기존에 ref 를 사용하던 코드를 다른 방법으로 해결하는 리팩토리를 해보면 장단점을 조금이라도 더 명확히 파악할 수 있을 것 같다) 추가로 위와 같은 생각을 하게 된 리액트 개발자들과 개발 커뮤니티의 이야기들을 첨언해보면..
    • useImperativeHandle / ref 의 사용을 최대한 피하자
    • 근거1 : ref를 사용하지 않아도 해결하려고 하는 문제들을 충분히 해결할 수 있기 때문
    • 근거2 : react는 선언형 프로그래밍이기에 ref를 사용한 명령형 프로그래밍 방식은 best practice가 아니다


❔책을 읽으며 궁금했던 것들

  • 🙋🏻‍♀️ useEffect를 넘길 때 빈배열을 넘길 때 다시 확인해보기
  • 초기 데이터 로딩, 이벤트 리스너 등록, 타이머 설정 상황에서 보통 빈배열을 넘기게 되는데 useEffect 를 사용하지 않고 구현할 수 있는 방법이 있을 지 고려해보라는 의미가 아니였을까?
  • 🙋🏻‍♀️ redux와 recoil의 상태값 변화에 따른 렌더링 방식
  • redux는 대규모 애플리케이션에 적합하며, 복잡한 상태 관리와 시간 여행 디버깅이 필요한 경우에 강점을 발휘하는 반면, recoil은 작은 규모의 프로젝트나 컴포넌트 중심의 상태 관리와 성능 최적화가 필요한 경우에 더 적합한 라이브러리
  • 둘 다 구독(subscribe)형식을 활용해서 각각 컴포넌트에 사용하는 useSelector와 useRecoilState(useRecoilValue)에서 상태에 변화가 있는지 (렌더링이 필요한지) 판단하고, 리렌더링을 발생시킨다. 외 더 자세한 렌더링 방식은 따로 정리해보기🤓
  • 🙋🏻‍♀️ useImperativeHandle의 다양한 사례
    • 여러 input을 관리할 때
    • 부모 컴포넌트에서 다른 컴포넌트 제출을 할 때 (보통 form) : 예시코드


📑 useState 의 게으른초기화 원리

  • React는 처음 useState가 호출될 때 해당 상태 변수를 생성하고 기본값을 사용하여 초기화를 딱 한 번 한다. (해당 변수와 연결된 값이 메모리에 저장된다.) 이때 초기화는 컴포넌트가 처음 렌더링 될 때만 발생하고, 이후 상태 변수가 생성되었다면 초기화를 하지 않음. 상태 초기화는 컴포넌트가 DOM 에서 완전히 삭제되지 않는 한 (unmount 되지 않는 한) 계속 유지된다. 따라서 무거운 연산 등의 이유로 실행 비용이 많이 드는 작업에서 최초 한 번만 실행되는 useState 의 초기화를 이용한 함수를 사용하는 것.
    • useState 내부엔 클로저가 존재하며, 클로저를 통해 값을 가져오고 초깃값은 최초에만 사용된다
    • 참고하면 좋을 useState 내부 동작 원리 레퍼런스 | 리액트 내부 동작 원리 - useState() : React Internals Deep Dive 번역본





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


상태 관리는 왜 필요한가?

  • 리액트 상태 관리의 역사
    • Flux 패턴의 등장 : 양방향 데이터 바인딩으로 인해 코드의 양이 많아지고 변경 시나리오가 복잡해지며 관리가 어려워지는 이유로 페이스북 팀은 단방향 데이터 흐름을 변경하는 것을 제안하는데, 이게 Flux 패턴의 시작
    • 시장 지배자 리덕스의 등장 : Elm 아키텍처의 영향을 받아 작성된 라이브러리로, Elm 코드의 주목할 포인트 3가지는 다음과 같음
    • 모델 : 애플리케이션의 상태
    • 뷰 : 모델을 표현하는 HTML
    • 업데이트 : 모델을 수정하는 방식
    • Context API 와 useContext
    • 훅의 탄생, 그리고 React Query 와 SWR
    • Recoil, Zustand, Jotai, Valtio 에 이르기까지


상태 관리는 왜 필요한가?

  • 가장 기본적인 방법 : useState 와 useReducer (React Hook 이용하기)
  • 상태 관리 라이브러리 Recoil Jotai Zustand 사용하기
  • 리렌더링을 만드는 방법은 거의 동일. 각 라이브러리별 특징을 잘 파악해서 현재 애플리케이션의 상황과 철학에 맞는 상태 관리 라이브러리를 적적하게 선택해 사용함으로써 효율적인 애플리케이션을 만드는 데 기여하기





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


리액트 개발 도구

  • 리액트 팀은 리액트 애플리케이션의 원활한 개발을 위한 개발 도구인 react-dev-tools 를 만들어 제공하고 있음
  • 리액트 개발 도구 설치 : 브라우저에 맞춰 브라우저 확장 도구로 설치해서 사용하기
    • 컴포넌트 : Components 탭은 정적인 현재 리액트 컴포넌트 트리의 내용을 디버깅하기 위한 도구. 현재 리액트 애플리케이션의 컴포넌트 트리를 확인할 수 있으며, 단순히 컴포넌트의 구조 뿐 아니라 props 와 내부 hooks 등 다양한 정보 확인 가능
    • 프로파일러 : Profiler 탭은 리액트가 렌더링하는 과정에서 발생하는 상황을 확인하기 위한 도구


💭 의견

  • 리액트 개발 도구는 사용해본 적이 거의 없는데 트리 구조를 파악할 때 유용하게 쓰일 것 같아서, 소스 코드를 실제로 보기 힘든 타 사이트의 리액트 코드를 보고싶을 때 사용해보면 좋을 것 같다





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


리액트 17 버전 살펴보기

  • 16 버전과 다르게 새롭게 추가된 기능이 없으며 호환성이 깨지는 변경사항(= 기존에 사용하던 코드의 수정을 필요로 하는)을 최소화했다는 것을 가장 큰 특징으로 뽑음
  • 주요 변경사항
    • 점진적인 업그레이드 지원 (애플리케이션 규모가 너무 크거나, 레거시 / 오래된 코드들이 실행되는 서비스에서 업그레이드에 대한 부담감을 사라지게 함)
    • 이벤트 위임 방식 변경 (이벤트가 document가 아닌 리액트 최상단 요소에 추가됨)
    • import React from ‘react’ 가 필요없음 (새로운 JSX transform 으로 tsconfig.json 의 jsx 를 react-jsx 등으로 변경하면 됨)
    • 이벤트 풀링 제거
    • useEffect 클린업 함수의 비동기 실행
    • 컴포넌트의 undefined 반환에 대한 일관적인 처리


리액트 18 버전 살펴보기

  • 주요 변경사항
    • 동시성 렌더링 지원
    • 새로 추가된 훅
    • useId : 컴포넌트별로 유니크한 값을 생성하는 새로운 훅
    • useTransition : UI 변경을 가로막지 않고 상태를 업데이트할 수 있게 해주는 훅
    • useDeferredValue : 리액트 컴포넌트 트리에서 리렌더링이 급하지 않은 부분을 지연할 수 있게 도와주는 훅
    • useSyncExternalStore : 외부 저장소의 변경사항을 구독할 수 있게 도와주는 훅 (상태관리 라이브러리를 작성하거나, 리액트 외부의 무언가에 대한 값이 필요하고, 값이 변경될때마다 리렌더링이 수행되어야하는 케이스에 사용하면 적절)
    • useInsertionEffect : layout Effects 가 실행되기 전에 전체 요소를 DOM 에 주입할 수 있게 해주는 훅 (CSS-IN-JS 를 위한 훅)
    • react-dom/client
    • createRoot : 기존 react-dom 의 render 메서드를 대체할 새로운 메서드
    • hydrateRoot : 서버 사이드 렌더링 애플리케이션에서 하이드레이션을 하기 위한 새로운 메서드
    • react-dom/server
    • renderToPipeableStream : Node.js 환경에서 리액트 컴포넌트를 HTML 로 렌더링 하는 메서드
    • renderToReadableStream : 웹 스트림(web stream) 기반으로 리액트 컴포넌트를 HTML 로 렌더링 하는 메서드
    • 자동 배치 제공 : 리액트가 여러 상태 업데이트를 하나의 리렌더링으로 묶어서 성능을 향상시키는 방법
    • 더욱 엄격해진 엄격 모드
    • 더 이상 안전하지 않은 특정 생명주기를 사용하는 컴포넌트에 대한 경고
    • 문자열 ref 사용 금지
    • findDOMNode에 대한 경고 출력
    • 구 Context API 사용 시 발생하는 경고
    • 예상치 못한 부작용 검사
    • Suspense 기능 강화
    • 인터넷 익스플로러 지원 중단에 따른 추가 폴리필 필요
    • 컴포넌트에서 undefined 를 반환해도 에러가 발생하지 않고, null 반환과 동일하게 처리됨.
    • <Suspense fallback={undefined} /> 도 null 과 동일하게 처리됨.
    • renderToNodeStream 이 지원 중단되고, renderToPipeableStream 사용이 권장됨.





✏️ 12장 : 모든 웹 개발자가 관심을 가져야 할 핵심 웹 지표


핵심 웹 지표

  • 핵심 웹 지표 (Core Web Vital) 란 구글에서 만든 지표로, 웹사이트에서 뛰어난 사용자 경험을 제공하는 데 필수적인 지표를 일컫는 용어로, 구글에서 핵심 웹 지표로 꼽는 지표는 아래와 같음.
    • 최대 콘텐츠풀 페인트 (LCP)
    • 최초 입력 지연 (FID)
    • 누적 레이아웃 이동 (CLS)
  • 핵심까진 아니지만, 특정 문제를 진단할 때 사용하기 유용한 것
    • 최초 바이트의 시간 (TTFB)
    • 최초 콘텐츠풀 시간 (FCP)


최대 콘텐츠풀 페인트 (LCP)

  • 페이지가 처음으로 로드를 시작한 시점부터 뷰포트 내부에서 가장 큰 이미지 또는 텍스트를 렌더링하는 데 걸리는 시간
  • 여기서 뷰포트는 사용자에게 현재 노출되는 화면을 의미하며, 뷰포트 내부에서 ‘큰 이미지와 텍스트’는 다음과 같이 정의되어 있음.
    • <img>
    • <svg> 내부의 <image>
    • poster 속성을 사용하는 <video>
    • url()을 통해 불러온 배경 이미지가 있는 요소
    • 텍스트와 같이 인라인 텍스트 요소를 포함하고 있는 블록 레벨 요소
    • 이 블록 레벨 요소에는 <p>, <div> 등이 포함됨.
    • 사용자에게 페이지의 정보를 화면에 전달하는 속도를 객관적으로 판단하기 위한 지표
    • LCP 최적화 참고 문서


최초 입력 지연 (FID)

  • 사용자가 페이지와 처음 상호 작용할 때 (예시 : 링크를 클릭하거나 버튼을 탭하거나 사용자 지정 JavaScript 기반 컨트롤을 사용할 때) 부터 해당 상호 작용에 대한 응답으로 브라우저가 실제로 이벤트 핸들러 처리를 시작하기까지의 시간을 측정
  • 웹사이트의 반응속도 / 반응성을 측정하는 지표
  • 구글은 사용자 경험을 크게 4가지로 분류하는데 이를 RAIL 이라고 함.
    • Response : 사용자의 입력에 대한 반응 속도, 50ms 미만으로 이벤트를 처리할 것
    • Animation : 애니메이션의 각 프레임을 10ms 이하로 생성할 것
    • Idle : 유휴 시간을 극대화해 페이지가 50ms 이내에 사용자 입력에 응답하도록 할 것
    • Load : 5초 이내에 콘텐츠를 전달하고 인터랙션을 준비할 것
  • FID 최적화 참고 문서


누적 레이아웃 이동 (CLS)

  • 사용자의 가시적인 콘텐츠에 영향을 미쳐야 하기 때문에 뷰포트 내부의 요소에 대해서만 측정하며, 뷰포트 밖의 요소에 대해서는 측정하지 않음.
  • 페이지의 생명주기 동안 발생하는 모든 예기치 않은 이동에 대한 지표를 계산하는 것을 누적 레이아웃 이동이라고 하는데, 해당 지표가 낮을 수록 더 좋은 웹사이트.
  • 누적 레이아웃 이동 점수를 계산할 때 포함되는 내용
    • 영향분율 : 레이아웃 이동이 발생한 요소의 전체 높이와 뷰포트 높이의 비율
    • 거리분율 : 레이아웃 이동이 발생한 요소가 뷰포트 대비 얼마나 이동했는 지
  • CLS 최적화 참고 문서





✏️ 13장 : 웹페이지의 성능을 측정하는 다양한 방법


애플리케이션에서 확인하기

  • create-react-app
    • reportWebVitals 함수 : 웹에서 성능을 측정하기 위한 함수로 web-vitals 라이브러리를 통해 여러 지표를 측정할 수 있음. web-vitals는 PerformanceObserver 라는 api 를 사용하는데, 웹 페이지에서 다양한 성능을 측정할 수 있도록 도와주는 API 로 브라우저에서 웹페이지의 성능을 측정하기 위해 사용된다. PerformanceObserver 를 지원하지 않는 브라우저에서는 web-vitals의 도움을 받아 성능을 측정하기 어려움
  • create-next-app
    • NextWebVitalsMetric 메서드 : next.js 에서 사용되는 메서드로 기본적인 핵심 웹 지표 외에도 아래와 같은 Next 에 특화된 추가 사용자 지표를 제공한다.
    • Next.js-hydration : 페이지가 서버 사이드에서 렌더링되어 하이드레이션하는 데 걸린 시간
    • Next.js-route-change-to-render : 페이지가 경로를 변경한 후 페이지를 렌더링하는 데 걸리는 시간
    • Next.js-render : 경로 변경이 완료된 후 페이지를 렌더링하는 데 걸린 시간


구글 라이트하우스

  • 구글에서 제공하는 웹 페이지 성능 측정 도구로, 오픈소스로 운영되고 있음. 핵심 웹 지표 뿐 아니라 접근성, PWA, SEO 등 웹 페이지를 둘러싼 다양한 요소들을 측정하고 점검할 수 있음. 구글라이트 실행 방법은 아래와 같음.
    • 브라우저 확장 프로그램 설치
    • 크롬 개발자 도구: 크롬의 개발자 도구에는 라이트하우스가 기본적으로 내장되어 있음.
    • CLI: lighthouse 를 npm 라이브러리를 이용해서 CLI 명령어로 지표를 수집할 수 있음.
    • 크롬을 실행해 성능 지표를 분석하고자 하는 사이트를 방문한 후 크롬 개발자 도구를 열어 Lighthouse 탭을 클릭 (다른 확장 프로그램 영향이 있을 수 있으니 시크릿창으로 실행해 확인하는 것을 권장)
  • 구글 라이트하우스 - 모드 종류
    • 탐색 모드 : 일반적으로 페이지에 접속했을 때부터 페이지 로딩이 완료될 때까지의 성능을 측정하는 모드로 지표 수집이 완료되면 리포트가 생성되는 걸 확인할 수 있는데 각 내용의 의미는 다음과 같다

    • 성능 : 웹페이지의 성능과 관련된 지표를 확인할 수 있는 영역으로 핵심 웹 지표인 최초 콘텐츠풀 페인트, 최대 콘텐츠풀 페인트, 누적 레이아웃 이동 외에도 지원하는 추가 지표가 있는데 이는 아래와 같음.

    • Time to Interactive : 페이지에서 사용자가 완전히 상호작용할 수 있을 때까지 걸리는 시간을 측정

    • Speed Index : 페이지가 로드되는 동안 콘텐츠가 얼마나 빨리 시각적으로 표시되는지를 계산

    • Total Blocking Time : 메인 스레드에서 특정 시간 이상 실행되는 작업으로 긴 작업이 수행될 때마다 메인 스레드가 차단된 것으로 간주.

    • 접근성 : 웹 접근성으로, 장애인 및 고령자 등 신체적으로 불편한 사람들이 일반적인 사용자와 동등하게 웹페이지를 이용할 수 있도록 보장하는 것을 뜻함.

    • 권장사항 : 웹사이트를 개발할 때 고려해야 할 요소들을 얼마나 지키고 있는 지 확인할 수 있는 지표로 보안, 표준 모드, 최신 라이브러리, 소스 맵 등 다양한 요소들이 포함되어 있음,

    • 검색 엔진 최적화 : 웹페이지가 구글과 같은 검색엔진이 쉽게 웹페이지 정보를 가져가서 공개할 수 있도록 최적화되어 있는지를 확인

    • 기간 모드 : 실제 웹페이지를 탐색하는 동안 지표를 측정하는 것으로, 기간 모드 시작을 누른 뒤 성능 측정을 원하는 작업을 수행한 다음, 기간 모드를 종료하면 그 사이 일어난 작업들에 대한 지표를 확인할 수 있음. (탐색 모드에서 지원한 지표들과 추가로 아래 지표를 지원함)

    • 흔적 : 웹 성능을 추적한 시간을 성능 탭에서 보여주는데, 시간의 흐름에 따라 어떻게 웹페이지가 로딩되었는 지 상세하게 보여줌

    • 트리맵 : 페이지를 불러올 때 함께 로딩한 모든 리소스를 함께 모아서 볼 수 있는 곳으로 리소스 중 어떠한 파일이 전체 데이터 로딩에서 어느 정도를 차지했는 지 비율로 확인할 수 있으며, 실제 불러온 데이터의 크기도 확인할 수 있음.

    • 스냅샷 모드 : 탐색 모드와 매우 유사하지만 현재 페이지 상태를 기준으로 분석하다는 점이 다름. 즉, 현재 상태에서 검색엔지의 최적화, 접근성, 성능 등을 분석할 수 있음. 페이지 로딩이 아닌 특정 페이지의 특정 상태를 기준으로 분석하고 싶다면 스냅샷 모드를 사용하면 됨. (스냅샷 모드의 지표는 탐색 모드와 매우 유사)



WebPageTest

  • 웹사이트 성능을 분석하는 도구로 가장 널리 알려진 도구로서, 무료 기능도 있지만 유료로 제공하는 분석 도구도 있을 만큼 웹사이트 성능을 분석할 수 있는 심도 있는 기능이 많음. WebPageTest 에서 제공하는 분석 도구는 크게 5가지로 아래와 같다
    • Site Performance : 웹사이트의 성능 분석을 위한 도구
    • Core Web Vitals : 웹사이트의 핵심 웹 지표를 확인하기 위한 도구
    • Lighthouse : 구글 라이트하우스 도구
    • Visual Comparison : 2개 이상의 사이트를 동시에 실행해 시간의 흐름에 따른 로딩 과정을 비교하는 도구
    • Traceroute : 네트워크 경로를 확인하는 도구
  • Performance Summary : 측정 결과 페이지는 크게 3가지 영역으로 나눠져 있으며, 자세한 영역은 클릭해서 확인 할 수 있음.
    • Opportunities & Experiments : 웹사이트에 대한 평가를 총 3가지로 나눠서 보여줌 (웹 사이트가 충분히 빠른 지 / 웹사이트의 사용성과 시각적인 요소를 확인 / 보안 취약성을 점검)
    • Observed Metrics : 최초 바이트까지의 시간, 렌더링 시작에 소요되는 시간, 최초 콘텐츠풀 페인트 등 측정할 수 있는 다양한 시간 지표에 대해 나타냄.
    • Individual Runs : 기본적으로 WebPageTest 는 3번의 테스트를 돌려서 평균값을 보여주는데, 각 실행별로 어떠한 결과를 보여주는지 확인할 수 있음.
  • Filmstrip : 웹사이트를 마치 필름을 보는 것처럼 시간의 흐름에 따라 어떻게 웹사이트가 그려졌는지, 또 어떤 시점에 어떤 리소스가 불러와졌는 지 볼 수 있는 메뉴
  • Details : Filmstrip 에서 보여준 내용을 자세하게 보여주는 영역
  • Web Vitals : 최대 콘텐츠풀 페인트(LCP), 누적 레이아웃 이동(CLS), 총 블로킹 시간(TBT)에 대한 자세한 내용을 확인할 수 있는 메뉴
  • Optimizations : 리소스가 얼마나 최적화되어 있는 지 확인할 수 있는 메뉴
  • Content : 웹사이트에서 제공하는 콘텐츠, 에셋을 종류별로 묶어 통계를 보여주는 메뉴로 에셋 종류별 크기와 로딩 과정을 확인할 수 있으며, 시간의 흐름에 따라 렌더링을 거치면서 또 어떻게 에셋을 불러오는지도 확인할 수 있음.
  • Domains : Content 메뉴에서 보여준 에셋들이 어느 도메인에서 왔는지를 도메인별로 묶어서 확인할 수 있는 메뉴
  • Detected Technologies : 웹사이트를 개발하는 데 사용된 기술을 확인할 수 있는 메뉴
  • Main-thread Processing : 메인 스레드의 작업을 스크립트 실행, 레이아웃, 리소스 로딩, 페인팅, 기타 총 5가지로 분류해서 알려주는 메뉴
  • Lighthouse Report : 구글 라이트하우스 리포트를 확인할 수 있음


크롬 개발자 도구

  • 성능 통계 (크롬 개발자 도구의 Performance Insights) : 웹사이트의 성능을 자세하게 확인할 수 있는 도구로, 라이트하우스와 비슷하게 Page Load 를 선택해 웹사이트 로딩 시작부터 끝까지를 확인하거나, 혹은 Start Recording 을 눌러서 원하는 액션을 수행하면서 웹사이트 성능을 측정할 수 있음.
    • Insights : 성능을 측정하는 기간 동안 발생한 이벤트 중 눈여겨봐야 할 내용을 시간의 흐름에 따라 모아서 보여줌
    • 메인 메뉴 : 성능을 측정하는 기간 동안 무슨 일이 일어나는 지 확인할 수 있는 다양한 기능을 제공
    • 성능 : 성능 분석에 사용하기 위해 만들어진 탭으로 다소 어렵고 복잡하지만 더 자세한 정보를 조금 더 세밀하게 확인해볼 수 있다는 장점이 있음.
    • 메뉴 : 성능 탭에서 사용할 수 있는 메뉴를 확인할 수 있음.
    • 요약 : 측정 기간의 CPU, 네트워크 요청, 스크린샷, 메모리 점유율 등을 요약해서 볼 수 있음
    • 네트워크 : 성능 측정 기간 동안 발생한 모든 네트워크 요청을 확인할 수 있음
    • 소요 시간과 기본 : 성능 탭의 핵심이라 부를 수 있는 부분으로, 시간의 흐름에 따라 메인 스레드의 작업은 어떻게 이뤄졌는지, 또 자바스크립트 힙 영역은 어떻게 변화하는 지 동을 확인할 수 있음





✏️ 14장 : 웹사이트 보안을 위한 리액트와 웹페이지 보안 이슈


리액트에서 발생하는 크로스 사이트 스크립팅 (XSS)

  • 크로스 사이트 스크립팅이란 웹사이트 개발자가 아닌 제3자가 웹사이트에 악성 스크립트를 삽입해 실행할 수 있는 취약점을 의미. 이 취약점은 일반적으로 게시판과 같이 사용자가 입력을 할 수 있고, 이 입력을 다른 사용자에게 보여줄 수 있는 경우에 발생
  • 리액트에서 XSS 이슈가 발생되는 경우
    • dangerouslySetinnerHTML prop
    • dangerouslySetinnerHTML 은 특정 브라우저 DOM의 innerHTML 을 특정한 내용으로 교체할 수 있는 방법으로 props 로 넘겨주는 __html 문자열에 제한이 없기 때문에 검증이 필요함.
    • useRef 를 활용한 직접 삽입
    • useRef 를 활용하면 직접 DOM 에 접근할 수 있으므로 dangerouslySetinnerHTML 과 비슷하게
    • 보안 취약점이 있는 스크립트를 삽입하면 동일한 문제가 발생함.
  • 리액트에서 XSS 문제를 피하는 방법 : 제3자가 삽입할 수 있는 HTML 을 안전한 HTML 코드로 한 번 치환하는 것. 이러한 과정을 새니타이즈 또는 이스케이프라고 하는데, 다양한 라이브러리를 활용하여 적용할 수 있음.


getServerSideProps 와 서버 컴포넌트를 주의하자

  • 서버에는 일반 사용자에게 노출되면 안 되는 정보들이 담겨 있기 때문에 클라이언트, 즉 브라우저에 정보를 내려줄 때는 조심해야 함.
  • getServerSideProps가 반환하는 props 값은 모두 사용자의 HTML 에 기록되고, 또한 전역 변수로 등록되어 스크립트로 충분히 접근할 수 있는 보안 위협에 노출되는 값이 됨. 또한 충분히 getServerSideProps 에서 처리할 수 있는 리다이렉트가 클라이언트에서 실행되어 성능 측면에서도 손해를 보게 됨. 따라서 getServerSideProps 가 반환하는 값 또는 서버 컴포넌트가 클라이언트 컴포넌트에 반환하는 props 는 반드시 필요한 값으로만 철저하게 제한되어야 함. 이는 보안 측면의 이점 뿐 아니라 성능 측면에서도 이점을 가져다 줄 수 있음.


<a> 태그의 값에 적절한 제한을 둬야 한다

  • <a> 태그는 반드시 페이지 이동이 있을 때만 사용하고, 페이지 이동 없이 어떠한 핸들러만 작동시키고 싶다면 <a> 보다는 <button>을 사용하는 것이 좋음
  • 피싱 사이트로 이동하는 것을 막기 위해 origin 확인 후 처리하는 것이 좋음


HTTP 보안 헤더 설정하기

  • HTTP 보안 헤더란 브라우저가 렌더링하는 내용과 관련된 보안 취약점을 미연에 방지하기 위해 브라우저와 함께 작동하는 헤더를 의미.
  • 보안 헤더 종류
    • Strict-Transport-Security 응답 헤더 : 모든 사이트가 HTTPS 를 통해 접근해야 하며, 만약 HTTP 로 접근하는 경우의 모든 시도는 HTTPS 로 변경되게 함.
    • <expire-time> 은 이 설정을 브라우저가 기억해야 하는 시간을 의미
    • includeSubDomains 가 있을 경우 이러한 규칙이 모든 하위 도메인에도 적용됨
    • X-XSS-Protection : 페이지에서 XSS 취약점이 발견되면 페이지 로딩을 중단하는 헤더로, 현재 사파리와 구형 브라우저에서만 제공되는 기능.
    • X-Frame-Options : 페이지를 frame, iframe, embed, object 내부에서 렌더링을 허용할 지 나타낼 수 있음
    • Permissions-Policy : 웹사이트에서 사용할 수 있는 기능과 사용할 수 없는 기능을 명시적으로 선언하는 헤더. 개발자는 다양한 브라우저의 기능이나 API를 선택적으로 활성화하거나 필요에 따라서는 비활성화할 수 있는데 여기서 말하는 기능이란 카메라나 GPS 와 같이 브라우저에서 제공하는 기능을 의미.
    • X-Content-Type-Options : Content-Type 에서 제공하는 MIME 유형이 브라우저에 의해 임의로 변경되지 않게 해주는 헤더로 웹서버가 브라우저에 강제로 이 파일을 읽는 방식을 지정하는 것.
    • Referrer-Policy : HTTP 요청에는 Referer 라는 헤더가 존재하는데 이 헤더에는 현재 요청을 보낸 페이지의 주소가 나타남. Referrer-Policy 헤더는 이 Referer 헤더에서 사용할 수 있는 데이터를 나타냄.
    • Content-Security-Policy : 콘텐츠 보안 정책(Content-Security-Policy, 이하 CSP)은 XSS 공격이나 데이터 삽입 공격과 같은 다양한 보안 위협을 막기 위해 설계되었음.
    • 대표적인 지시문 종류
    • src : font-src, img-src, script-src 등 다양한 src 를 제어할 수 있는 지시문
    • form-action : 폼 양식으로 제출할 수 있는 URL을 제한할 수 있음.
  • 보안 헤더 설정하기
    • Next.js 에선 next.config.js 파일에서 애플리케이션 보안을 위해 HTTP 경로별로 보안 헤더를 적용할 수 있음. 레퍼런스
    • 정적인 파일을 제공하는 NGNIX 의 경우 경로별로 add_header 지시자를 사용해 원하는 응답 헤더를 추가할 수 있음
  • 보안 헤더 확인하기 : Analyse your HTTP response headers 사이트를 방문해 헤더를 확인하고 싶은 웹사이트의 주소를 입력하면 현재 보안 헤더 상황을 알 수 있음. 레퍼런스


취약점이 있는 패키지의 사용을 피하자

  • Snyk Vulnerability Database | Snyk 를 방문해 사용하는 패키지 이름으로 검색해 보면 현재 라이브러리의 취약점을 한눈에 파악할 수 있으므로 패키지 보안 이슈를 추적하는 데 많은 도움이 됨.


OWASP Top 10

  • OWASP 은 Open Worldwide Web Application Security Project라는 오픈소스 웹 애플리케이션 보안 프로젝트를 의미하는데 주로 웹에서 발생할 수 있는 정보 노출, 악성 스크립트, 보안 취약점 등을 연구하며, 주기적으로 10대 웹 애플리케이션 취약점을 공개하는데 이를 OWASP Top 10 이라고 함. OWASP Top Ten | OWASP Foundation