공부

리액트 Docs - 고급 안내서

ko.reactjs.org/docs/accessibility.html

 

접근성 – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

접근성

 

- www.wuhcag.com/wcag-checklist/WCGA 체크리스트를 통해 접근성을 갖춘 웹사이트를 만드는데 필요한 지침 확인

 

 


코드 분할

 

- 대부분의 리액트 앱은 Webpack으로 여러 파일을 하나로 병합한 번들 된 파일을 웹 페이지에 포함하여 한번에 전체 앱을 로드한다.

 

- 번들링은 훌륭하지만 앱이 커지면 하나의 번들파일도 커진다. 번들이 거대해지는 것을 방지하기 위한 좋은 해결방안은 나누는 것이다.

 

- 코드 분할은 런타임에 여러 번들을 동적으로 만들고 불러오는 것으로 웹팩에서 지원한다.

 

- 동적 import()문법으로 코드분할하기

//Before
import { add } from './math';

console.log(add(16, 26));

//After
import("./math").then(math => {
  console.log(math.add(16, 26));
});

웹팩에서 이 구문을 만나면 앱의 코드를 분할한다. CRA나 Next.js는 이미 웹팩이 구성되어있기 때문에 즉시 사용할 수 있다.

 

- React.lazy

import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

Suspense의 자식으로 React.lazy로 import한 컴포넌트를 주면 코드 분할이 적용된다. fallback prop은 컴포넌트가 로드될 때까지 기다리는 동안 렌더링하려는 React 엘리먼트이다. 하지만 아직 서버사이드 렌더링에는 적용되지 않는다. SSR에는 github.com/gregberge/loadable-components 이 라이브러리를 추천한다.

 

 

 

 

- Route-based code splitting

 

코드 분할을 어느 곳에 도입할지는 까다롭다. 시작하기 좋은 장소로 라우트를 추천한다.

예시

import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route exact path="/" component={Home}/>
        <Route path="/about" component={About}/>
      </Switch>
    </Suspense>
  </Router>
);

 


Context

 

- 컨텍스트를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있다.

 

- 일반적인 React 애플리케이션에서 데이터는 위에서 아래로 props를 통해 전달되지만, 애플리케이션 안의 여러 컴포넌트들에 전해줘야하는 props의 경우 (선호 locale, ui 테마 등) 이 과정이 번거로울 수 있다. 이때 컨텍스트를 사용하면 좋다.

 

- 컨텍스트는 리액트 컴포넌트 트리 안에서 전역적으로 데이터를 공유하기 위해 고안된 방법이다.

 

- context를 사용하면 컴포넌트를 재사용하기가 어려워진다.

 

- 여러 레벨에 걸쳐 props 넘기는 걸 대체하는 데에 context보다 컴포넌트 합성이 더 간단한 해결책일 수 있다.

 

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

//context 객체 생성
const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

- createContext() 메서드로 컨텍스트 객체를 만든다.

 

- 컨텍스트 객체를 구독하고 있는 컴포넌트를 렌더링 할 때 리액트는 트리 상위에서 가장 가까이 있는 짝이 맞는 Provider로부터 현재값을 읽는다.

 

- Provider는 컨텍스트를 구독하는 컴포넌트들에게 컨텍스트 변화를 알리는 역할을 한다. context를 구독하는 모든 컴포넌트는 provider의 value가 바뀔 때마다 다시 렌더링된다.

 

- provider로부터 하위 consumer로의 전파는 React.memo 또는 shouldComponentUpdate 메서드가 적용되지 않으므로, 상위 컴포넌트가 업데이트를 건너 뛰더라도 consumer가 업데이트 한다. 

 

- 훅스의 useContext는 클래스 컴포넌트에서의 Context.Consumer역할을 한다. context의 변화를 구독한다.

 

- usecontext를 호출한 컴포넌트는 context값이 변경되면 항상 리렌더링된다. 리렌더링에 비용이 많이 든다면 해결책 github.com/facebook/react/issues/15156#issuecomment-474590693 참고.

    1) 컨텍스트 분할시키기.

    2) 구성요소를 둘로 분할 memo하고 그 사이에 배치

function  Button ( )  { 
  let  appContextValue  =  useContext ( AppContext ) ; 
  let  theme  =  appContextValue . 테마 ;  // "선택기" 
  return  < ThemedButton  theme = { theme } / > 
} 

const  ThemedButton  =  memo ( ( {  theme  } )  =>  { 
  // 나머지 렌더링 로직 
  return  < ExpensiveTree className = { 테마 } / > ; 
} ) ;

useContext가 Button을 렌더시켜도 Button에서 호출되는 ThemedButton이 memo되어 있으므로 props값이 변하지 않으면 렌더링되지않는다.

 

    3) useMemo 내부가 있는 구성요소

function  Button ( )  { 
  let  appContextValue  =  useContext ( AppContext ) ; 
  let  theme  =  appContextValue . 테마 ;  // "선택자" 

  return  useMemo ( ( )  =>  { 
    // 나머지 렌더링 로직 
    return  < ExpensiveTree  className = { theme } / > ; 
  } ,  [ theme ] ) 
}

 

 


Fragments

 

- 컴포넌트가 여러 엘리먼트를 반환하기 위한 패턴.

 

- DOM에 별도의 노드를 추가하지 않고 여러 자식을 그룹화할 수 있다.

 


Fowarding Refs

 

- 자식의 refs를 전달하는 기술이다.

 

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

- 부모에서 ref를 만들고 ref를 넘겨준다. 자식은 forwardRef 메서드로 ref를 받고, 본인의 엘리먼트의 ref를 부모에게 받은 ref에 연결시킨다. 만약 forwardRef 메서드를 사용하지 않았으면 <FancyButton> 컴포넌트 자체에 ref가 걸린다.

 

- 굳이 직접 만들고 싶다면 ref의 이름을 다른이름으로 바꿔서 props로 전달시키고 자식에서 ref를 연결시키면된다.

 


고차 컴포넌트

 

- 컴포넌트 로직을 재사용하기 위한 리액트의 기술이다. 리액트 API의 일부분이 아니라, 컴포넌트 성격에서 나타난 패턴.

 

class CommentList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // "DataSource" is some global data source
      comments: DataSource.getComments()
    };
  }

  componentDidMount() {
    // Subscribe to changes
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // Clean up listener
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // Update component state whenever the data source changes
    this.setState({
      comments: DataSource.getComments()
    });
  }

  render() {
    return (
      <div>
        {this.state.comments.map((comment) => (
          <Comment comment={comment} key={comment.id} />
        ))}
      </div>
    );
  }
}

 

class BlogPost extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      blogPost: DataSource.getBlogPost(props.id)
    };
  }

  componentDidMount() {
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    this.setState({
      blogPost: DataSource.getBlogPost(this.props.id)
    });
  }

  render() {
    return <TextBlock text={this.state.blogPost} />;
  }
}

 

- 두 컴포넌트는 대상이 다를 뿐, 대부분의 메서드 동작 방식은 동일하다.

 

- 많은 컴포넌트에서 이 로직을 공유할 수 있게 하는 추상화가 필요하고 이때 고차 컴포넌트가 탁월하다.

 

 

function withSubscription(WrappedComponent, selectData) {
  // ...and returns another component...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ... that takes care of the subscription...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }

    render() {
      // ... and renders the wrapped component with the fresh data!
      // Notice that we pass through any additional props
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}


const CommentListWithSubscription = withSubscription(
  CommentList,
  (DataSource) => DataSource.getComments()
);

const BlogPostWithSubscription = withSubscription(
  BlogPost,
  (DataSource, props) => DataSource.getBlogPost(props.id)
);

 

- withSubScription 함수로 공통되는 기능을 분리시키고, 재사용한다.

 

- 주의사항

    - render 메서드 안에서 고차 컴포넌트를 사용하면 안된다. 렌더링 메서드 안에서 컴포넌트에 고차 컴포넌트를 적용할 수 없다. 콤포넌트를 다시 마운트하면 고차 컴포넌트의 state와 모든 하위 항목이 손실된다.

 

    - 정적 메서드는 복사해야 한다. 

    - ref는 전달할 수 없다. props가 아니라 key처럼 리액트에서 특별히 처리함.


성능 최적화

 

- 내부적으로 리액트는 UI를 최신화하기 위해 비용이 많이 드는 DOM 작업의 수를 최소화하기 위해 몇 가지 기발한 방법을 슨다. 많은 애플리케이션에서 리액트를 사용하면 성능을 특별히 최적화하기 위해 많은 작업을 수행하지 않고도 빠른 사용자 인터페이스로 이어질 수 있다. 그럼에도 불구하고 리액트 앱의 속도를 높일 수 있는 몇 가지 방법이 있다.

 

- 프로덕션 빌드 사용하기. 리액트에는 유요한 경고들이 많이 포함되어 개발하는데 유용하다. 하지만 리액트를 더 크고 느리게 만들기 때문에 앱을 배포할 때 프로덕션 버전을 사용해야 한다. cra에선 npm run build하면되고 사이트에서 뭐 여러가지 빌드도구들을 소개함.

 

 

Chrome performance 탭으로 컴포넌트 프로파일링 하기 -> 이거 뭔지 봤는데 어떻게 성능을 체크하는지 잘 모르겠다. 리렌더링 되는 여부만 파악하는건가? 따로 medium.com/wantedjobs/react-profiler%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%84%B1%EB%8A%A5-%EC%B8%A1%EC%A0%95%ED%95%98%EA%B8%B0-5981dfb3d934 나중에 이글 읽기

 

- 긴 목록을 가상화하기. 앱에서 긴 목록을 렌더링 하는 경우 windowing이라는 기법을 사용하라. 이 기법은 주어진 시간에 목록의 부분 목록만 렌더링하며 컴포넌트를 다시 렌더링하느 데 걸리는 시간과 생성된 DOM 노드의 수를 크게 줄일 수 있다. react-window.now.sh/#/examples/list/fixed-size 또는 bvaughn.github.io/react-virtualized/#/components/List 같이 windowing을 지원하는 라이브러리가 있다.

 

 

재조정 피하기

 

- 리액트는 렌더링 된 UI의 internal representation을 빌드하고 유지 관리한다. 여기에는 컴포넌트에서 반환되는 리액트 엘리먼트가 포함된다. representation은 리액트가 js 객체에서의 작업보다 느릴 수 있기 때문에 필요에 따라 DOM 노드를 만들고 기존 노드에 접근하지 못하도록 한다. RN에서도 같은 방식으로 동작한다.

 

- 컴포넌트의 prop이나 state가 변경되면 리액트는 새로 반환된 엘리먼트를 이전에 렌더링된 엘리먼트와 비교해서 실제 dom 업데이트가 필요한지 여부를 결정한다. 같지 않을 경우 react는 dom을 업데이트 한다.

 

- 리액트가 변경된 dom 노드만 업데이트하더라도 리렌더링에는 여전히 다소 시간이 걸린다. 대부분의 경우 문제가 되지 않지만 속도 저하가 눈에 띄는 경우 다시 렌더링 시작하기 전까지 실행되는 생명주기 함수 shouldComponentUpdate로 리렌더링을 무시함으로써 속도를 높일 수 있다.

 

 


Profiler API

 

위에서 나중에 다시봐야지했는데 바로 이 개념에 대한 챕터가 나왔다.. 저 미디움 블로그 게시글이랑 같이 정리함

 

- Profiler는 리액트 앱이 렌더링하는 빈도와 렌더링 비용을 측정한다. Profiler의 목적은 메모이제이션 같은 성능 최적화 방법을 활용할 수 있는 애플리케이션의 느린 부분들을 식별해내는 것이다.

 

- 리액트 문서에서랑 다르게 Profiler 컴포넌트로 감싸지 않아도 react developer tool 크롬 확장 프로그램을 쓰면 확인가능하다.

 

 

medium.com/wantedjobs/react-profiler%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EC%84%B1%EB%8A%A5-%EC%B8%A1%EC%A0%95%ED%95%98%EA%B8%B0-5981dfb3d934

 

React Profiler를 사용하여 성능 측정하기

Profiler 도구를 사용하여 React 성능을 측정하고, 어떻게 하면 성능을 높일 수 있는지 알아보겠습니다.

medium.com

 

 

사이트 예제로 프로파일링 확인

 

 

App이 로드되는데 걸리는 시간이 7.7ms 이다.

 

 

 

3.7초 시점에 데이터를 패치해서 렌더링하는데 2855ms가 걸림

 

너무 짧은건 기록안되는 경우도 있는듯하다. List에서 모든 리스트아이템이 출력되는건 아니었고 첫커밋인 0초 시점에서 포토1과 2의 메시지가 타이틀이 출력되었지만 기록되지 않았음.

 

좀 큰 컴포넌트들 언제 리렌더링되고 렌더링하는데 걸리는 시간은 어떤지 체크하긴 좋은 것 같다.

 


재조정

 

- 리액트는 선언적 api를 제공하기 때문에 갱신이 도리 때마다 매번 무엇이 바뀌었는지를 걱정할 필요가 없다. 이는 앱 작성을 쉽게 만들어주지만, 리액트 내부에서 어떤 일이 일어나고 있는지 명확히 눈에 보이지 않는다. 이 글에서 우리가 리액트의 비교 알고리즘을 만들 때 어던 선택을 했는지를 소개한다. 이 비교 알고리즘 덕분에 컴포넌트의 갱신이 예측 가능해지면서도 고성능 앱이라고 불러도 될 정도의 빠른 앱을 만들 수 있다.

 

- 하나의 트리를 가지고 다른 트리로 변환하기 위한 최소한의 연산 수를 구하는 알고리즘은 최첨단 알고리즘도 O(n^3)가 걸린다. 리액트에 이 알고리즘을 적용한다면 1000 개의 엘리먼트를 그리기 위해 10억번의 비교 연산을 수행해야한다...

 

- 리액트는 대신 두가지 가정을 기반하여 O(n)의 휴리스틱 알고리즘을 구현했다.

1) 서로다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.

2) 개발자가 key prop을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해 줄 수 있다.

 

 

비교 알고리즘

 

- 두 개의 트리를 비교할 때, 리액트는 두 엘리먼트의 루트 엘리먼트부터 비교한다. 이후의 동작은 루트 엘리먼트의 타입에 따라 달라진다.

 

 

엘리먼트의 타입이 다른 경우

 

- 두 루트 엘리먼트의 타입이 다르면, 리액트는 이전 트리를 버리고 완전히 새로운 트리를 구축한다. 트리를 버릴 때 이전 DOM 노드들은 모두 파괴된다. 컴포넌트 인스턴스는 componentWillUnmount()가 실행된다.

 

- 새로운 트리가 만들어 질 때, 새로운 DOM 노드들이 DOM에 삽입된다. 그에 따라 인스턴스는 componentWillMount()가 실행되고 componentDidMount()가 이어서 실행된다. 이전 트리와 연관된 state는 모두 사라진다.

 

- 루트 아래의 모든 컴포넌트도 언마운트 되고, state도 사라진다.

 

 

DOM 엘리먼트의 타입이 같은 경우

 

- 같은 타입의 두 리액트 돔 엘리먼트를 비교할 때, 리액트는 두 엘리먼트의 속성을 확인하여, 동일한 내역은 유지하고 변경된 속성들만 갱신한다. 예를들어 같은 div 엘리먼트의 className만 다르다면 리액트는 현재 DOM 노드 상에 className만 수정한다. style도 마찬가지다.

 

- DOM 노드의 처리가 끝나면, 리액트는 이어서 해당 노드의 자식들을 재귀적으로 처리한다.

 

 

같은 타입의 컴포넌트 엘리먼트

 

- 컴포넌트가 갱신되면 인스턴스 동일하게 유지되어 렌더링 간 state가 유지된다.

 

- 리액트는 새로운 엘리먼트의 내용을 반영하기 위해 현재 컴포넌트 인스턴스의 props를 갱신한다. 이때 해당 인스턴스의 componentWillReceiveProps() 와 componentWillUpdate()를 호출한다. 그 다음 render 메소드를 호출한다.

 

 

자식에 대한 재귀적 처리

 

<ul>
  <li>first</li>
  <li>second</li>
</ul>

<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

ul의 자식의 끝에 엘리먼트를 추가하면 두 트리사이의 변경은 잘 작동한다.

firse끼리 비교하고, second끼리 비교하고 그 다음 third가 없으니 추가시킨다.

 

<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

하지만 ul의 첫부분에 새 자식 엘리먼트를 추가하면 성능이 좋지않다.

Duke - Connectincut / Villanova - Duke / Villanova 이렇게 순서대로 비교후 다르다고 판단해서 자식을 변경한다. 이러한 비효율은 문제가 될 수 있다.

 

이러한 문제를 해결하기 위해 리액트는 key 속성을 지원한다.

<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

같은 키의 자식끼리 비교하기 때문에 자식들의 순서가 바뀌어도 자식들이 일치하는지 알 수 있다.

 

이러한 이유로 키값에 인덱스를 넣는 것은 좋지않다. 항목이 재배열되면 위의 문제가 발생한다.

 

인덱스를 key로 사용 중 배열이 재배열되면 컴포넌트의 state와 관련된 문제가 발생할 수 있다. 컴포넌트 인스턴스는 key를 기반으로 갱신되고 재사용된다. 인덱스를 key로 사용하면, 항목의 순서가 바뀌었을 때 key 또한 바뀔 것이고, 그 결과로 컴포넌트의 state도 엉망이 된다.

예시 codepen.io/pen?&editors=0010&prefill_data_id=ddf55157-bd06-4fb4-8073-1e259bf67311

 

Create a New Pen

...

codepen.io

 

휴리스틱 주의할 점

 

1. 알고리즘은 다른 컴포넌트 타입을 갖는 종속 트리들의 일치 여부를 확인하지 않는다. 만약 매우 비슷한 결과물을 출력하는 두 컴포넌트를 교체한다면 그 둘을 같은 타입으로 만드는게 낫다.

 

2. key는 반드시 변하지 않고, 예상 가능하며, 유일해야 한다.

 

'공부' 카테고리의 다른 글

코드가 얼마나 복잡한가에 대한 근거  (0) 2021.01.12
CORS  (0) 2020.12.29
리액트 Docs - 주요개념  (0) 2020.12.15
실용적인 프론트엔드 전략 3  (0) 2020.12.10
실용적인 프론트엔드 테스트 전략 - 2  (0) 2020.12.09