[React] React 기본

hyeondoonge·2023년 10월 23일
1
post-thumbnail

정의

웹상의 UI를 구축하기위한 javascript 라이브러리

장단점

장점

  • 컴포넌트 기반으로 앱을 구축을 한다. 재사용성, 코드 가독성 향상에 이점을 가진다.
  • 선언적인 API 제공하고, 이는 개발생산성 향상에 도움된다.
    • Hooks API를 이용한 생명주기 접근
    • Suspense, ErrorBoundary 등 선언적으로 사용할 수 있는 API
  • JSX 문법 이용함으로써 직관적인 UI 구축 가능하다.
  • 데이터 흐름이 위에서 아래로 흐르기 때문에 예측하기 쉽다.
  • 신뢰성있는 라이브러리이다. 구 Facebook 개발했고, 현재까지도 활발한 업데이트와 버그 개선이 이뤄지고있다.
  • 친절한 공식문서를 제공한다. 최근 공식문서가 개편되어 예제가 더 풍부해졌다.
  • 많은 유저를 갖고있고, 기업에서도 많이 사용하는 라이브러리로 배워두면 기여할 수 있는 곳이 많다.

단점

  • SEO에 좋지 않다. 기본적으로 CSR을 위한 라이브러리기 때문이다. 하지만 Next.js를 사용하여 React로도 SSR이 가능하여 이를 보완할 수 있다.
  • 트랜스컴파일 비용이 든다. JSX 문법을 JS로 변환하는 작업이 필요하기 때문이다.
  • Virtual DOM을 관리하고 비교하는 비용이 든다.
  • React와 조합할 수 있는 도구가 많다. 따라서 팀 내에서 React 내에서 함께 사용할 상태관리, 스타일링 도구, 라우터 구성 등에 대한 의사소통이 추가로 이뤄져야한다.

Virtual DOM

  • 실제 DOM과 대비되어 변경사항 파악을 위해 존재하는 DOM이다.
  • 초기 앱 실행 또는 상태 변경을 호출하면 Virtual dom을 만들어낸다.
  • 이전 Virtual DOM과 현재 Virtual DOM을 비교하여 변경이 발생한 부분만을 실제 DOM에 일괄적으로 반영함으로써 최소한의 연산을 통해 성능을 개선한다. 아래에서 설명하는 재조정과정에서 Virtual DOM간의 비교를 수행한다.

재조정(Reconciliation)

트리를 비교하고 변경사항을 적용하는 동작으로 Diffing Algorithm, 자식 list간 key비교를 통해 빠르게 변경된 부분을 찾아, 변경사항을 트리에 반영한다.

Diffing Algorithm
O(n)의 시간복잡도을 통해 효과적으로 UI를 갱신할 수 있는 알고리즘이다. 비교를 수행할 때 두 엘리먼트 트리의 루트부터 비교를 시작한다. 재귀적으로 비교하다보면 아래와 같이 4가지 케이스를 만날 수 있는데, 각 케이스에 대해서 다음과 같이 처리한다.

  1. 엘리먼트 타입이 다른 경우
    이전 트리를 버리고 완전히 새로운 트리를 구축한다. 이에 따라 기존 컴포넌트는 unmount되고 새로운 컴포넌트를 mount한다.
    <div>
    	<Counter />
    </div>
    
    <span>
    	<Counter />
    </span>
  1. DOM 엘리먼트의 타입이 같은 경우
    동일한 내용은 유지하고하고 DOM 노드 상에 className, style등 변경 속성만 갱신한다. 그리고 자식에 대해 재귀적으로 처리한다. 아래와 같은 케이스의 경우 위의 엘리먼트의 className 변경과 style의 display 속성을 실제 DOM 노드에 추가한다.
    <div className="open" style={{ backgroundColor: '#1995C0' }} />
    
    <div className="close" style={{ backgroundColor: '#1995C0', display: 'none' }} />
  1. 컴포넌트 엘리먼트의 타입이 같은 경우
    인스턴스를 동일하게 유지함으로서 렌더링 간 state를 유지한다. 또한 현재 컴포넌트 인스턴스의 props를 갱신하여 새로운 내용을 반영한다. 이후 자식에 대해 재귀적으로 처리한다.

자식 list 비교

  • 자식 list간의 key를 비교하여 새로운 key가 발견되면 새로운 노드를 생성한다. 또는 기존에 있었던 key라면 새로운 노드를 생성하지않고 내용을 수정한다.
  • 변경이 발생하지 않은 element는 변경 또는 유지한다.

상태 변경 동작 원리

React 가장 핵심은 상태 관리가 아닌가 싶다. 우리가 React와 같은 라이브러리를 사용하는 이유가 상태를 지지고 볶아서 사용자가 보기 쉽게하기 위함이기 때문이다. 또한 사용자의 상호작용이 일어나면 상태를 변화시켜 UI에 변화를 주는 것이 필요하다.

이를 이해하면 React가 어떻게 상태변경을 실제 DOM에 반영을 하는지, 그리고 왜 렌더링 최적화가 필요한지에 대해 알 수 있게 될 것이다.

크게 3가지 과정 Trigger => Render => Commit을 거쳐 진행된다.

1. Trigger

state변경을 통해 render를 트리거한다. 컴포넌트에서 setState 함수를 이용해 상태를 변경하여 리렌더링을 발생시키는 과정이다.

2. Render

react 단에서 변경이 발생한 컴포넌트를 호출한다.
초기 렌더링이라면 컴포넌트 트리의 root부터 렌더링하고, 이후에 리렌더링이 트리거된 지점부터 컴포넌트를 호출하며 이를 재귀적으로 수행한다.
이 과정에서 변경이 발생한 부분을 파악하고, 해당 정보를 다음 단계인 Commit에서 활용한다.

Optimizing

  • 동작원리에 따르면 기본적으로 React는 trigger가 발생한 컴포넌트를 기점으로 전부 리렌더링하는데, 이는 최적화된 동작방식이 아니다. 트리가 크고, 변경되지 않아도되는 컴포넌트가 많을 때 문제가된다.
  • 따라서! 이러한 문제가 발생하여 앱에 성능적인 저하가 있다면 React에서 제공하는 최적화 도구 useCallback, useMemo, memo를 통해 개선할 수 있다.

3. Commit

react 단에서 변경 사항을 실제 DOM에 반영하는 과정이다. 초기 렌더링 시에는 appendChild를 통해 모든 돔 노드를 추가한다. 이후 렌더링 시에는 최소한 연산을 통해 DOM을 업데이트하기위해, Render 과정에서 파악한 변경된 부분만을 실제 DOM에 반영한다.

React only changes the DOM nodes if there’s a difference between renders. - React 공식문서

세 단계의 과정이 끝나면 브라우저 렌더링을 통해 최종적으로 사용자가 보는 브라우저 화면에 렌더링하게된다.

데이터 흐름

  • 위에서부터 아래로 데이터를 전달하는 단방향 데이터 흐름을 따른다. 이와 같은 단순한 데이터 흐름을 갖는 것은 앱의 복잡도를 낮추는 특징 중 하나라고 할 수 있겠다.
  • 모든 state를 특정 컴포넌트가 소유하고, 해당 state는 트리구조에서 아래에 있는 컴포넌트에만 영향을 미친다.
  • 특정한 컴포넌트에서 리렌더링이 발생한 것은 해당 컴포넌트를 포함한 상위 state가 변경된 것으로 볼 수 있다.
    function Product({ productId }) {
    	return <Review productId={productId}/>
    }
    
    function ProductList () {
    	const [product, setProduct] = useState();
    	return {products.map((product) => <Product productId={product.productId} />)}
    }

JSX

  • javascript를 확장한 문법으로, xml 문법과 javascript 함께 사용하여 렌더링 로직을 작성하게한다. JSX는 UI 구조의 가독성 및 개발생산성을 향상한다는 이점이 있다.
  • 트랜스파일하면 React의 createElement로 변환된다. 즉 JSX문법은 React.createElement의 Syntactic Sugar라고할 수 있다.
  • XSS 공격 방지기능이 있다.
    • cross-site-scripting 스크립트 언어가 입력되어 효능을 발휘하는 것을 제한하고자, jsx로 삽입된 모든 값은 렌더링 전 이스케이프한다.

아래와 같이 JSX 문법을 사용하고 트랜스파일 한 결과를 비교하면, 컴포넌트 코드 작성 시 해당 문법으로 우리가 더 생산성있게 구현할 수 있음을 느낄 수 있다.

  • 트랜스파일 전
    import App from './src/components/App.js'
    
    function Component() {
     return <div>hello!</div> 
    }
  • 트랜스파일 후
    import App from './src/components/App.js';
    function Component() {
      return /*#__PURE__*/React.createElement("div", null, "hello!");
    }

참고

데이터는 아래로 흐릅니다 - React legacy 공식문서
재조정(Reconciliation) - React legacy 공식문서
Redner and Commit - React 공식문서

0개의 댓글

관련 채용 정보