안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

리액트 (React)

[React] 공식 문서 요약 - 고급 안내서

피그브라더 2020. 9. 9. 14:31

* React 공식 문서의 '고급 안내서' 부분을 읽으면서 개인적으로 요약한 내용들입니다. 잘못된 내용이 있다면 지적 부탁드립니다.

 

1. 코드 분할 (Code-Splitting)

 

코드 분할 – React

A JavaScript library for building user interfaces

ko.reactjs.org

  • 대부분의 React 앱들은 여러 정적 파일들을 Webpack, Rollup, Browserify 등의 번들러로 번들링 한 파일들을 웹 페이지에 포함하여 한 번에 앱 전체를 로드할 수 있다. 이러한 종류의 앱을 SPA(Single Page Application)라고 부른다.
  • Create React App, Next.js, Gatsby 등의 툴을 사용한다면 자동으로 Webpack이 설치되고 적절히 설정까지 되어 있을 것이다. 만약 이러한 툴을 사용하지 않는다면 직접 Webpack 등의 번들러를 설치하고 설정까지 해줘야 한다.
  • 실제로 Create React App으로 생성한 React 프로젝트의 package.json 파일을 살펴보면, "npm start" 명령어를 실행할 시 "react-scripts"라는 Node.js 패키지에 포함된 start.js 파일을 실행시켜서 Webpack 관련 설정을 하고 Webpack 개발 서버를 실행하도록 되어 있다. 이는 파일의 변동사항이 감지될 때마다 Webpack으로 새로 번들링을 수행하고 웹 페이지를 자동 리로드 한다.
  • 번들링은 그 자체로 충분히 훌륭하지만, 앱의 규모가 커질수록 번들링 된 파일의 크기도 증가한다는 문제가 있다. 특히 서드 파티 라이브러리를 사용할 때 실수로 앱이 상당히 커져서 로드 시간이 길어지는 문제가 발생할 수도 있다.
  • 코드 분할(Code Splitting)은 이러한 문제를 해결한다. 필요한 순간에만 동적으로 로드해도 되는 특정 코드들은 번들링 파일과는 별도로 분리시켜두는 것이다. 이는 지연 로딩을 통해 성능을 획기적으로 향상시키며, 앱의 초기화 로딩에 필요한 비용을 줄여준다.
  • 기본적으로 Webpack, Rollup, Browserify 등의 번들러는 이러한 코드 분할 기능을 지원하고 있다.
  • 코드 분할을 도입하는 가장 기본적인 방법은 동적 import() 함수를 사용하는 것이다.
    • EX) import('./math').then(math => console.log(math.add(16, 26)));
    • Webpack(Babel이 아니라)은 해당 함수의 호출 문을 마주쳤을 때 코드 분할을 시작한다.
    • Create React App이나 Next.js를 사용한다면 이러한 설정은 이미 되어 있으므로 즉시 사용 가능하다.
    • Webpack의 코드 분할 기능에 관해서는 여기를, 이를 위한 Webpack의 설정 방법은 여기를 참고하자.
    • 만약 Babel을 사용하고 있다면, Babel이 동적 import() 함수를 인식은 하되 변환하지는 않도록 설정해줘야 한다. (참고)
  • React.lazy() : 동적 import() 함수를 이용하여 렌더링 할 컴포넌트를 로드하는 함수
    • EX) const OtherComponent = React.lazy(() => import('./OtherComponent'));
    • 해당 컴포넌트가 필요한 순간에 동적으로 로딩되도록 한다.
    • 동적 import() 함수를 호출하는 함수를 인자로 받는다. 이 함수는 컴포넌트를 Export Default로 내보내는 모듈로 결정될 Promise 객체를 반환해야 한다. Named Export로 내보내지는 컴포넌트를 로드하려면 중간 모듈을 하나 거치도록 수정해야 한다.
    • Lazy 컴포넌트는 Suspense 컴포넌트 하위에서 렌더링 되어야 하며, Suspense 컴포넌트는 Lazy 컴포넌트가 로드되기 전까지 보여줄 엘리먼트를 fallback prop으로 전달받아야 한다. '로딩 중'의 기능이라고 생각하면 된다.
    • 네트워크 장애 등의 문제로 모듈의 동적 로딩에 실패할 수도 있다. 이러한 경우 에러 경계(Error Boundary)를 만들고 Suspense 컴포넌트를 감싸도록 하면 UX 및 복구 관리 등을 처리할 수 있다.
  • 코드 분할을 시작하기에 가장 좋은 곳은 바로 라우트이다. 대부분의 웹 사용자들은 페이지 간의 이동에 어느 정도 시간이 걸리는 것에 익숙하며, 보통의 경우 페이지 전체를 한 번에 렌더링 하는 경향이 있어서 렌더링과 동시에 페이지의 다른 부분과 상호작용할 일도 거의 없기 때문이다. 예시는 이곳을 참고하자.

 

2. Context

 

Context – React

A JavaScript library for building user interfaces

ko.reactjs.org

  • Context를 이용하면 트리의 여러 레벨에 존재하는 컴포넌트들이 props를 통하지 않고도 동일한 데이터를 공유할 수 있다.
  • 만약 UI 테마, 로그인한 유저의 정보, 선호 언어 등의 데이터를 일일이 props로 곳곳에 전달하면 코드가 지저분해질 것이다.
  • 단, 특정 데이터들을 필요로 하는 컴포넌트가 단순히 깊은 레벨에 위치한 것이라면, 해당 컴포넌트 엘리먼트 자체를 prop으로 전달하는 합성 패턴도 고려해볼 만하다. Context는 특정 데이터를 필요로 하는 컴포넌트가 여러 레벨에 분산되어 있을 때 훨씬 유용하다.
  • Context API
    • React.createContext() : Context 객체를 만드는 함수이다. React는 해당 Context 객체를 구독하는 컴포넌트를 렌더링 할 때 Context의 값(= 뒤에서 알아볼 value 또는 this.context의 값)을 트리 상위에서 가장 가까운 곳에 있는 Provider에서 참조한다. 인자로 전달받는 값은 트리 안에서 적절한 Provider를 찾지 못했을 때 사용하는 디폴트 값에 해당한다.
    • Context.Provider : Context 객체를 구독하는 컴포넌트들에게 Context의 변화를 알리는 컴포넌트이다. value prop을 전달받아서 이 값을 하위 컴포넌트들에게 전달한다. 하위에 또 다른 Provider를 배치하는 것도 가능하며, 이 경우 하위 Provider의 값이 우선시된다. Provider의 value prop이 바뀔 때마다 하위에서 해당 Context 객체를 구독하는 모든 Consumer(Context.Consumer 컴포넌트, contextType 프로퍼티가 지정된 컴포넌트, useContext() 함수를 사용한 컴포넌트)들은 다시 렌더링된다(= render() 함수 호출). 참고로 Provider에서 Consumer들로의 전파는 shouldComponentUpdate() 메소드가 적용되지 않으므로, 상위 컴포넌트가 업데이트를 스킵하더라도 Consumer들은 반드시 업데이트된다.
    • Class.contextType : 클래스의 contextType 프로퍼티로 Context 객체를 지정하면, 해당 클래스 안에서 this.context를 이용하여 Context의 값을 가장 가까운 Provider로부터 참조할 수 있다. 참고로 이 방식은 하나의 Context만 구독할 수 있다. 만약 여러 Context를 구독해야 한다면 Context.Consumer를 사용해야 한다.
    • Context.Consumer : Context 객체를 구독하려는 컴포넌트를 감싸는 컴포넌트이다. 함수 컴포넌트에서 Context의 값을 읽기 위해 이 방법을 사용할 수 있다. Context.Consumer의 자식은 반드시 함수여야 한다. 이 함수는 Context의 현재 값을 받고 렌더링할 엘리먼트를 반환한다. 이 함수가 인자로 전달받는 값은 가장 가까운 Provider의 value prop 값과 같다. Provider의 value prop이 바뀔 때마다 해당 함수를 호출하여 새로 렌더링을 시도하게 된다.
    • Context.displayName : Context 객체에 displayName 프로퍼티를 지정해주면 React Dev Tool에서 해당 Context와 관련된 컴포넌트들(Provider, Consumer 등)에 표시되는 이름을 지정해줄 수 있다.
  • Context를 활용한 여러 예시들은 여기를 참고하자.
  • 주의) Provider의 value prop에 객체를 직접 전달하지 말자. 그러면 렌더링할 때마다 매번 새로운 객체가 생성되어 참조값이 달라지면서 불필요한 리렌더링을 유발할 수 있다. 대신에 value로 전달할 값을 state로 끌어올려서 관리하자.

 

3. 에러 경계 (Error Boundaries)

 

에러 경계(Error Boundaries) – React

A JavaScript library for building user interfaces

ko.reactjs.org

  • 과거에는 UI, 즉 컴포넌트 내에서 JavaScript 에러가 발생하면 컴포넌트 내부의 상태가 망가지면서 앱 전체에 영향을 주곤 했다. 그런데 React는 이와 같이 컴포넌트 내에서 발생하는 에러를 처리할 수 있는 방법을 제공하지 않았기에 복구조차 불가능했다.
  • React 16에서 도입한 에러 경계(Error Boundary)는 이러한 종류의 에러를 처리하기 위한 컴포넌트를 의미한다.
  • 에러 경계는 자식 컴포넌트 트리의 생성자 혹은 생명주기 메소드에서 발생하는 JavaScript 에러들을 기록하고 깨진 컴포넌트 대신에 폴백 UI를 보여주는 역할을 수행하는 컴포넌트이다. 이는 컴포넌트 단위로 기능하는 catch 블록으로 비유할 수 있다.
  • 단, 에러 경계는 이벤트 핸들러, 비동기 코드, 서버 사이드 렌더링, 혹은 에러 경계 자신에서 발생하는 에러는 캐치하지 못한다. 특히 에러 경계 자신에서 발생하는 에러의 경우, 처리되지 못한 채 다시 상위에 존재하는 에러 경계로 전파가 된다.
  • static getDerivedStateFromError() 혹은 componentDidCatch() 생명주기 메소드를 정의하면 에러 경계 컴포넌트가 된다. (예시)
    • static getDerivedStateFromError(error) : 에러가 발생했을 때 폴백 UI를 렌더링 해줄 수 있다.
    • componentDidCatch(error, error_info) : 발생한 에러의 정보를 기록하는 데 사용할 수 있다.
  • 에러 경계를 얼마나 세분화할지는 개발자의 몫이다. 최상위 컴포넌트를 에러 경계로 감싸서 에러 메시지를 띄우도록 할 수도 있고, 각 위젯을 에러 경계로 감싸서 그것들이 앱의 다른 부분들과 충돌하지 않도록 보호할 수도 있다.
  • 에러 경계를 도입한 React 16부터는 컴포넌트 내부에서 발생한 에러가 캐치되지 않을 시 전체 컴포넌트 트리가 언마운트 된다. 손상된 UI를 보여주는 것보다 아예 안 보여주는 것이 훨씬 좋은 사용자 경험이라고 판단했기 때문이다. 이로 인해 React 16으로 업그레이드하는 앱은 기존에 발견되지 않던 문제를 발견하게 될 수도 있다.
  • Component Stack Traces
    • React 16부터는 렌더링 과정에서 발생하는 모든 에러들을 콘솔에 출력해준다.
    • 이때 에러 메시지와 JavaScript Stack뿐만 아니라 Component Stack Trace들도 보여줌으로써 컴포넌트 트리의 정확히 어디에서 에러가 발생했는지 표시해준다.
    • 만약 Create React App 프로젝트라면 Component Stack Trace에서 파일 이름과 라인 넘버도 표시가 된다.
    • Create React App이 아니라면 특정 플러그인을 Babel 설정에 직접 추가해줌으로써 이를 실현할 수 있다.
    • 참고로 이러한 기능은 개발을 위해서만 사용하고, 배포 시에는 비활성화해야 한다는 것을 기억하자.
  • try, catch 블록이 명령형 코드에서만 동작한다면, 에러 경계는 선언적 특성의 React 코드에 어울리는 에러 처리 메커니즘이다. 즉, 트리의 깊은 곳 어디에선가 생명주기 메소드 동작에 에러가 발생하면 어떤 방법으로든 이는 가장 가까운 에러 경계에 도달한다.
  • 앞서 말했듯 에러 경계는 이벤트 핸들러에서의 에러를 캐치하지 못한다. 이벤트 핸들러의 동작은 렌더링 과정에서 일어나는 것이 아니기 때문에 에러 경계에서 처리할 필요가 없는 것이다. 그러한 에러가 발생하더라도 화면의 모습은 변하지 않는다. 만약 이벤트 핸들러에서의 예외를 처리하고 싶으면 일반적인 try, catch 블록을 사용하면 된다.

 

4. Ref 전달하기 (Forwarding Refs)

 

Forwarding Refs – React

A JavaScript library for building user interfaces

ko.reactjs.org

  • React.forwardRef() 함수를 사용하여 전달받은 ref prop을 내부 엘리먼트에 전달하는 기법을 말한다.
  • 기본적인 캡슐화의 원칙에 따르면 각 컴포넌트는 서로의 내부 구현에 대해 알지 못한다.
  • 그러나 재활용성이 상당히 높은 Leaf 컴포넌트들(EX. FancyButton)의 경우 이러한 캡슐화가 오히려 불편할 수 있다. 이러한 컴포넌트들은 <input>, <button> 등의 DOM과 같이 애플리케이션 곳곳에서 유사한 방식으로 사용되는 경향이 짙으며, 포커스, 선택, 애니메이션 등의 기능을 위해 그것들의 내부 DOM 노드에 접근하는 것이 불가피한 경우도 많기 때문이다.
  • React.forwardRef() 함수 : 전달받은 ref prop을 내부 엘리먼트에 전달해주고자 하는 컴포넌트를 정의할 때 호출한다.
  • EX) const FancyButton = React.forwardRef((props, ref) => <button ref={ref}>{props.children}</button>);
  • 만약 기존의 컴포넌트 라이브러리에서 이 기법을 도입한다면, 이를 큰 변화로 인식하고 새로운 버전을 배포하자. 기존 라이브러리의 동작 방식을 크게 바꿀 것이고, 이는 해당 라이브러리에 의존하던 앱들에 상당한 영향을 미칠 것이기 때문이다. 또한, React.forwardRef() 함수를 지원하는 경우에만 이를 사용하도록 도입하는 것도 권장되지 않는다. 해당 라이브러리를 사용하던 앱이 React 자체를 업그레이드하는 순간 그것에 의해 동작이 크게 달라질 수 있기 때문이다.
  • HOC가 반환하는 컴포넌트는 전달받은 ref prop을 컨테이너 컴포넌트에 연결한다. ref는 일반적인 prop으로 처리되지 않기 때문에 만약 해당 ref를 표현 컴포넌트에 연결하고 싶다면 React.forwardRef() 함수를 이용하여 HOC를 약간 수정해줘야 한다. (참고)
  • React.forwardRef() 함수가 반환하는 컴포넌트는 인자로 전달되는 함수의 특성에 따라 React Dev tool에서 표시되는 이름이 결정된다. (참고)
    • 익명 함수인 경우 : "ForwardRef"
    • 이름이 명시된 함수인 경우 : "ForwardRef(함수명)"
    • 함수의 displayName 프로퍼티가 지정된 경우, 이름은 그 값을 따른다.

 

5. Fragments

 

Fragments – React

A JavaScript library for building user interfaces

ko.reactjs.org

  • <React.Fragment>는 실제 DOM에 마운트 되지 않는 엘리먼트로, <> 엘리먼트로 간단히 표현할 수도 있다.
  • 물론 <React.Fragment>도 엘리먼트이므로 실제로 엘리먼트 트리의 한 자리를 차지한다는 걸 기억하자. 따라서 재조정 과정에서 서로 다른 <React.Fragment> 엘리먼트의 하위에 존재하는 엘리먼트들은 서로 다른 트리에 존재하는 자식 엘리먼트로 인식된다.
  • 이를 활용하면 하나의 컴포넌트가 여러 엘리먼트들을 반환하도록 할 수 있다. (기본적으로는 단일 엘리먼트만 반환 가능)
  • 만약 key가 필요하다면 반드시 <> 엘리먼트가 아니라 <React.Fragment> 엘리먼트를 사용해야 한다.

 

6. 고차 컴포넌트 (High-Order Components)

 

Higher-Order Components – React

A JavaScript library for building user interfaces

ko.reactjs.org

  • 고차 컴포넌트(Higher-Order Component, HOC)란 컴포넌트를 인자로 받아서 새로운 컴포넌트를 반환하는 순수 함수이다.
  • 일반적으로 여러 컴포넌트들이 공통으로 사용하는 특정 로직을 재활용하기 위해 사용하는 패턴이다.
  • 서드 파티 라이브러리에서 자주 사용되는 패턴이다. (EX. Redux의 connect 함수의 반환 값, Relay의 createFragmentContainer 함수)
  • 컨테이너 패턴이란 상태나 구독 등을 담당하는 고수준의 컨테이너 컴포넌트와 렌더링만을 담당하는 저수준의 표현 컴포넌트를 분리하는 방식을 말한다. 컨테이너 컴포넌트는 렌더링에 필요한 데이터만 표현 컴포넌트에 props로 전달하게 된다. 이는 상태나 구독 등을 담당하는 특정 기술(EX. Redux)과는 독립적으로 표현 컴포넌트의 재사용성을 확보할 수 있다는 이점이 있다. HOC는 바로 이러한 컨테이너 패턴을 활용한 사례이다. 즉 HOC는 매개변수화 된 컨테이너 패턴이라고 볼 수 있다.
  • HOC는 인자로 전달받은 기존의 컴포넌트(= 표현 컴포넌트)를 컨테이너 컴포넌트 안에 래핑하고 그 컨테이너 컴포넌트를 반환한다. 컨테이너 컴포넌트는 여러 컴포넌트들이 공통으로 사용할 로직을 담고 있으며, 전달받은 props와 함께 추가적으로 자기 자신의 state나 메소드 등을 표현 컴포넌트에 props로 넘겨주게 된다. 참고로 HOC도 함수의 일종이기 때문에, 표현 컴포넌트 말고도 다른 추가 인자들을 받을 수도 있다. 일반적으로 그러한 추가 인자들은 컨테이너 컴포넌트의 내부 로직을 상황에 따라 다르게 정의하기 위해 사용이 된다(= 매개변수화 된 컨테이너).
  • HOC는 반드시 순수 함수(Pure Function)여야 한다. 즉, 인자로 전달받는 표현 컴포넌트는 절대로 건드리지 않는 것이 원칙이다.
  • 컨테이너 컴포넌트와 표현 컴포넌트는 오로지 props를 통해서만 통신하기 때문에, 동일한 props만 넘겨준다면 컨테이너 컴포넌트 내부의 로직이 수정되어도 문제없다. 그리고 이는 해당 컨테이너 컴포넌트와 통신하는 모든 표현 컴포넌트들에 일괄 반영될 것이다.
  • HOC Convention (관습)
    • 컨테이너 컴포넌트에서만 쓰는 props를 제외하고 나머지 props는 전부 표현 컴포넌트에게 전달한다.
    • 표현 컴포넌트만 인자로 전달받는 단일 인자 HOC는 Composability를 극대화한다. (참고)
    • 편한 디버깅을 위해 컨테이너 컴포넌트의 displayName 프로퍼티를 지정해준다. HOC가 컨테이너 컴포넌트를 반환하기 직전에 해당 프로퍼티의 값을 설정해주면 된다. 보통 래핑 하고자 하는 표현 컴포넌트의 이름을 활용하여 컨테이너 컴포넌트의 이름을 지어주게 된다. (참고)
  • HOC Caveats (주의 사항)
    • HOC를 render() 함수 안에서 사용하지 않아야 한다. React의 재조정 알고리즘은 이전 렌더링의 컴포넌트와 새로운 렌더링의 컴포넌트를 비교하여 같으면 수정하고, 다르면 이전 컴포넌트는 언마운트 하고 새로운 컴포넌트를 마운트 한다. render() 함수 안에서 HOC를 사용하면, render() 함수가 호출될 때마다 새로운 컴포넌트가 정의되는 것이므로 매번 언마운트 및 마운트를 반복하게 된다. 이는 성능 면에서도 매우 비효율적일 뿐 아니라, 기존 컴포넌트의 state가 유실된다는 문제가 발생한다. 따라서 HOC는 컴포넌트 정의문 밖에서만 사용하도록 하며, 동적으로 HOC를 사용해야만 하는 경우에는 생성자나 생명주기 메소드 안에서 사용하는 것을 권장한다.
    • HOC가 반환하는 컨테이너 컴포넌트는 표현 컴포넌트의 static 메소드를 가지지 않는다. 따라서 HOC는 컨테이너 컴포넌트를 반환하기 직전에 표현 컴포넌트의 static 메소드를 복사해줘야 한다. 만약 무슨 static 메소드를 가질지 미리 알 수 없다면 hoist-non-react-statics 라이브러리를 활용할 수 있다. 혹은 해당 static 메소드를 컴포넌트에서 분리 및 추출하여 사용해도 된다.
    • HOC가 반환하는 컨테이너 컴포넌트는 key, ref prop을 표현 컴포넌트에 전달할 수 없다. 이들은 일반적인 prop으로 처리되지 않기 때문이다. ref prop을 전달받은 컨테이너 컴포넌트가 이를 표현 컴포넌트에 그대로 전달해주려면 React.forwardRef() 함수를 사용해야 한다.

 

7. JSX 이해하기 (JSX In Depth)

 

JSX 이해하기 – React

A JavaScript library for building user interfaces

ko.reactjs.org

  • 문자열 리터럴을 prop으로 넘겨줄 때, 그 값은 HTML 이스케이프 처리가 되지 않는다. {'XXX'} 형식으로 전달할 때는 XXX가 그대로 전달이 되고, "XXX" 형식으로 전달할 때는 &nbsp, &ensp, &emsp, &lt, &gt 등과 같은 이스케이프 문자들만 변환이 된 뒤 전달이 된다. 예를 들어 &lt는 <로 변환된 뒤 전달이 된다. 이 원칙은 엘리먼트 사이에 적히는 문자열 리터럴에도 동일하게 적용된다. 이것도 prop의 일종이기 때문이다.
  • prop에 어떤 값도 명시하지 않을 경우, 기본값은 {true}이다. 하지만 명시해주는 것이 권장된다.
  • 전개 연산자를 사용하면 특정 객체의 프로퍼티들을 props로 넘겨줄 수 있다. (EX. {...other_props})
  • 엘리먼트와 엘리먼트 사이에 적히는 문자열 리터럴의 경우, 처음과 끝의 공백은 사라지며 중간에 끼어 있는 개행 문자는 한 개의 공백 문자로 대체된다. 단, 이는 문자열 리터럴과 문자열 리터럴 사이에 끼어 있는 개행 문자일 경우에만 그렇다. 즉, 문자열 리터럴과 JS 표현식 또는 엘리먼트 사이에 끼어 있는 개행 문자는 무시(삭제)된다.
  • 보통 JSX에 자식으로 삽입하는 JS 표현식은 문자열, 엘리먼트, 엘리먼트 배열 중 하나인 경우가 많다. 그러나 children도 prop의 일종이기 때문에 어떠한 JS 표현식도 전달될 수는 있다. 함수도 그중 하나이다. (EX. Context.Consumer)
  • true, false, null, undefined 값은 렌더링 되지 않는다. 즉 빈 문자열로 렌더링 된다.

 

8. Portals

 

Portals – React

A JavaScript library for building user interfaces

ko.reactjs.org

  • 기본적으로 컴포넌트가 반환하는 엘리먼트는 부모 DOM 노드의 가장 가까운 자식으로 마운트 된다.
  • 그러나 Portal을 사용하면 임의의 DOM 노드에 엘리먼트를 마운트 하는 것이 가능하다.
  • 컴포넌트가 ReactDOM.createPortal(자식 엘리먼트, DOM 노드) 함수의 반환 값인 Portal 엘리먼트를 반환하면 된다.
  • 이때 Portal 엘리먼트는 마운트만 다른 곳에 되었을 뿐, 일반 엘리먼트와 마찬가지로 엘리먼트 트리에서는 계층 구조를 유지한다. 이로 인해 Portal 엘리먼트 내부에서 발생하는 이벤트는 엘리먼트 트리의 계층 구조에서 상위로 전파(버블링)될 수 있다. 실제 DOM에서는 상위가 아닐지라도 말이다. 이는 개발의 추상화를 가능케 한다. Portal 엘리먼트를 반환하는 컴포넌트를 렌더링 하는 부모는 해당 컴포넌트가 Portal 엘리먼트를 반환하는지 아닌지 알고 있을 필요가 없기 때문이다. (참고)
  • 부모 컴포넌트에 "overflow: hidden" 또는 "z-index" 스타일이 적용된 경우, 혹은 자식을 튀어나오게 할 때 많이 사용한다.
  • EX) 다이얼로그, 호버 카드, 툴팁 등

 

9. ES6 없이 사용하는 React (React Without ES6)

 

ES6 없이 사용하는 React – React

A JavaScript library for building user interfaces

ko.reactjs.org

  • 클래스형 컴포넌트 정의 : ES6의 클래스 대신 create-react-class 라이브러리의 createReactClass() 함수 활용
  • createReactClass() 함수의 인자로 전달되는 객체의 형태
    • render 프로퍼티 : 렌더링 할 엘리먼트를 반환하는 함수
    • getDefaultProps 프로퍼티 : 각 prop의 기본값을 나타내는 객체를 반환하는 함수
    • getInitialState 프로퍼티 : state의 초깃값을 반환하는 함수
    • 이밖에도 추가적으로 원하는 프로퍼티를 지정 가능하다.
  • 자동 바인딩 : ES6의 클래스는 메소드에 this가 자동으로 바인딩되지 않지만, createReactClass() 함수에 전달되는 객체의 프로퍼티로 표현되는 각 함수들에는 자동으로 this가 바인딩된다. createReactClass() 함수가 객체를 반환하기 전에 미리 바인딩을 해주는 것이다.

 

10. JSX 없이 사용하는 React (React Without JSX)

 

JSX 없이 사용하는 React – React

A JavaScript library for building user interfaces

ko.reactjs.org

  • JSX의 태그 문법은 React.createElement() 함수를 호출하기 위한 문법적 설탕에 불과하다. 즉 꼭 사용해야만 하는 건 아니다.
  • React.createElement(component, [props], [...children])
    • component : 엘리먼트 타입 (문자열, 함수형/클래스형 컴포넌트, 또는 React.Fragment)
    • props : props를 나타내는 객체로, prop이 없다면 null을 적는다. (children prop은 여기에 포함되지 않음)
    • children : 문자열, 엘리먼트, 혹은 문자열/엘리먼트의 배열 (인자 개수는 1개 이상) → 자식 엘리먼트를 하드 코딩해서 나열했다면 복수 인자로 넘어가고, 프로그래밍적으로 배열을 만든 거라면 배열이 단일 인자로 넘어간다. 그러나 결국 그 둘도 본질적으로는 같다는 것을 기억하자. 작성 방법만 다를 뿐이다.

 

11. Ref와 DOM (Refs and the DOM)

 

Ref와 DOM – React

A JavaScript library for building user interfaces

ko.reactjs.org

  • Ref는 render() 메소드에서 생성된 DOM 노드나 컴포넌트 인스턴스에 접근하는 방법을 제공한다.
  • Ref의 바람직한 사용 사례 (일반적인 경우에서는 Ref 사용을 지양하며, props를 사용한 선언적인 접근을 권장)
    • 포커스, 텍스트 선택 영역, 미디어의 재생 등을 관리할 때
    • 애니메이션을 직접적으로 실행시킬 때
    • 서드 파티 DOM 라이브러리를 React와 함께 사용할 때
  • Ref 생성 : React.createRef() 함수의 호출을 통해 생성되어 엘리먼트의 ref 어트리튜브에 부착된다. 일반적으로 컴포넌트의 생성자에서 생성한 Ref를 자신의 프로퍼티에 추가함으로써 컴포넌트 인스턴스의 어느 곳에서도 해당 Ref에 접근할 수 있도록 한다.
  • Ref 접근 : 어떤 유형의 엘리먼트에 부착되었는지에 따라 Ref의 current 프로퍼티 값은 달라진다.
    • DOM 엘리먼트에 Ref가 부착된 경우 : Ref의 current 프로퍼티는 해당 DOM 노드를 가리킨다.
    • 클래스형 컴포넌트 엘리먼트에 Ref가 부착된 경우 : Ref의 current 프로퍼티는 해당 컴포넌트 인스턴스를 가리킨다.
    • 함수형 컴포넌트 엘리먼트에는 Ref를 부착할 수 없다. 컴포넌트 인스턴스가 없기 때문이다.
  • Ref가 부착된 엘리먼트는 마운트 될 때 current 프로퍼티에 해당 DOM 노드 또는 컴포넌트 인스턴스를 연결하고, 언마운트 될 때 다시 current 프로퍼티의 값을 null로 초기화한다. 이 작업은 componentDidMount() 또는 componentDidUpdate() 생명 주기 메소드가 호출되기 전에 진행된다.
  • 함수형 컴포넌트 엘리먼트에 Ref를 부착할 수는 없지만, 함수형 컴포넌트 안에서 Ref를 생성하고 엘리먼트에 이를 부착하는 것은 가능하다. 이때는 useRef() 함수의 호출을 통해 Ref를 생성한다. 참고로 이 Ref에 접근하는 내부 함수를 만들어서 특정 핸들러로 부착하는 경우, 해당 내부 함수가 정의되는 순간 생성되는 클로저(범위 객체)에 해당 Ref가 저장되어 있다는 것도 이해하면 좋다. (참고)
  • 부모 컴포넌트에게 Ref를 노출시키기 (= 자식 컴포넌트 내부의 DOM 노드 혹은 컴포넌트 인스턴스에 접근하기)
    • React.forwardRef() 함수를 사용하여 부모 컴포넌트에서 생성한 Ref를 자식 컴포넌트 내부의 엘리먼트에 전달해줘야 한다.
    • 이는 권장하지 않는 패턴이지만 종종 유용하다. 물론 findDOMNode()를 대신 사용할 수도 있지만, 이는 권장하지 않는다.
  • 콜백 Ref
    • Ref를 설정하기 위한 또 다른 방법으로, Ref가 설정되고 해제되는 상황을 세세하게 다룰 수 있는 함수를 일컫는다.
    • 컴포넌트 인스턴스의 프로퍼티에 DOM 노드 또는 컴포넌트 인스턴스를 지정하는 함수를 만들고 ref 어트리뷰트에 부착한다. 
    • 이 함수를 Ref 콜백이라 부르며, 부착된 엘리먼트에 해당하는 DOM 노드 또는 컴포넌트 인스턴스를 인자로 받아 동작한다.
    • Ref 콜백이 지정된 엘리멘트가 마운트 될 때 Ref 콜백이 해당 DOM 노드 또는 컴포넌트 인스턴스를 인자로 받아서 호출되며, 언마운트 될 때는 다시 null을 인자로 받아서 호출된다. 이 작업은 componentDidMount() 또는 componentDidUpdate() 생명 주기 메소드가 호출되기 전에 진행된다.
    • 주의) Ref 콜백이 인라인 함수로 선언된다면, 이는 업데이트 과정 중 처음에는 null을 인자로 받아서, 그다음에는 DOM 노드나 컴포넌트 인스턴스를 인자로 받아서 총 두 번 호출된다. 이는 매 렌더링마다 해당 함수가 새로 생성되어 기존의 Ref를 제거하고 새로운 Ref를 설정해야 하기 때문이다. 따라서 웬만하면 Ref 콜백을 바인딩된 메소드 형태로 선언하는 것이 좋다.
  • 문자열 Ref : 곧 사라질 예정이므로 createRef() 또는 콜백 Ref를 사용하도록 한다.

 

12. Render Props

 

Render Props – React

A JavaScript library for building user interfaces

ko.reactjs.org

  • 렌더링 로직을 컴포넌트 내부에 작성하는 것이 아니라, 렌더링 로직을 정의한 함수를 prop으로 전달받아서 호출하는 것을 말한다.
  • 즉, 무엇을 렌더링 할지 컴포넌트에 알려주는 함수를 의미하며, 일반적으로 render라는 이름을 사용한다.
  • 대부분의 HOC에 Render Prop 패턴을 이식할 수 있다. HOC에서는 컨테이너 컴포넌트가 자신의 state 혹은 메소드를 표현 컴포넌트에게 props로 전달하면서 렌더링 한다. 이를 Render Prop 패턴으로 바꾼다면, 해당 state 혹은 메소드를 인자로 받은 후 이를 props로 전달받는 컴포넌트 엘리먼트를 반환하는 함수를 render prop으로 사용하면 된다. 바깥에서 무슨 엘리먼트를 렌더링 할지 알려주면, render props를 전달받는 컴포넌트가 자주 사용되는 공통의 로직을 해당 엘리먼트에게 반영시킨 뒤 이를 반환하는 것이다. (참고)
  • 주의) render prop으로 전달하는 함수는 매번 새로 생성되므로 React.PureComponent를 상속하는 이점이 사라진다. 이를 방지하기 위해서는 render prop으로 전달하는 함수를 인스턴스 메소드로 정의해놓는 방식을 채택해야 한다. 만약 그렇게 할 수 없는 경우에는 React.Component를 상속하도록 바꾸는 걸 권장한다. 괜히 얕은 비교에 의한 성능 저하만 일으키기 때문이다.

 

13. 정적 타입 검사 (Static Type Checking)

 

Static Type Checking – React

A JavaScript library for building user interfaces

reactjs.org

  • Flow, TypeScript와 같은 정적 타입 체커들은 컴파일 타임에 타입 관련 문제를 찾아낼 수 있다. 또한 이는 IDE에서의 자동완성과 같은 기능을 추가로 제공하여 개발자의 작업 효율도 향상시킨다. 따라서 규모가 어느 정도 있는 프로젝트에서는 PropTypes를 사용하는 것보다 Flow 혹은 TypeScript를 사용하는 것이 권장된다.
  • Flow (Developed by Facebook)
    • JavaScript 코드를 위한 정적 타입 체커로, 보통 React와 함께 사용한다.
    • 변수, 함수, React 컴포넌트에 타입 주석을 작성함으로써 에러를 조기에 발견할 수 있다. (자세한 내용은 공식 문서를 참조)
    • Flow 사용 방법 (3단계)
      • 1st) Flow 패키지를 설치하여 프로젝트 의존성에 추가한다.
        • "npm install --save-dev flow-bin" 혹은 "yarn add -dev flow-bin" 명령어로 최신 버전의 Flow를 설치한다.
        • 추후 터미널에서 Flow를 사용하기 위해 package.json 파일의 "scripts" 부분에 "flow": "flow"를 추가한다.
        • "npm run flow init" 혹은 "yarn run flow init" 명령어로 Flow 설정 파일을 생성한다.
      • 2nd) 컴파일된 코드에서 Flow의 타입 관련 문법이 제거되도록 설정한다.
        • Flow는 타입 주석을 위한 문법과 함께 JavaScript 언어를 확장한다. 하지만 브라우저는 이 문법을 알지 못하기 때문에 컴파일된 JavaScript 코드를 브라우저에 그대로 보내면 안 된다. 즉, Flow의 타입 관련 문법을 제거해줘야 한다.
        • 이 작업은 JavaScript를 어떠한 도구로 컴파일하느냐에 따라 달라진다.
        • 만약 Create React App 프로젝트라면, Babel이 Flow의 타입 관련 문법을 제거하도록 이미 설정이 되어 있다.
        • 그렇지 않다면 직접 Babel을 설정해주기 위해 특별한 Babel 프리셋을 설치해줘야 한다.
          • "npm install --save-dev @babel/preset-flow" 혹은 "yarn add --dev @babel/preset-flow" 명령어로 Flow 프리셋을 설치한다.
          • 설치된 Flow 프리셋(@babel/preset-flow)을 Babel 설정 파일(.babelrc)의 "presets" 부분에 추가한다.
        • Create React과 Babel 모두 사용하지 않는다면 flow-remove-types 패키지를 통해 타입 주석을 제거할 수 있다.
      • 3rd) 타입 주석을 작성하고 타입 체크를 위해 Flow를 실행한다.
        • "yarn flow" 혹은 "npm run flow" 명령어로 Flow를 백그라운드에서 실행한다.
        • 정상적으로 실행이 되었다면 "No errors! Done in 0.17s."와 같은 메시지가 출력되어야 한다.
        • 타입 체크를 수행할 파일의 최상단에 "// @flow" 주석을 작성한다. Flow는 이러한 파일들만 체크한다. (물론 주석에 상관없이 모든 파일들을 체크하는 옵션을 줄 수도 있다.)
        • 이제 타입 체크가 필요하다고 판단되는 부분에 개발자가 Flow의 타입 관련 문법(타입 주석)을 작성해주면 된다.
  • TypeScript (Developed by Microsoft)
    • JavaScript의 슈퍼셋이다. 즉, 기존의 JavaScript 문법에 정적 타입 체크 관련 문법을 추가하여 만든 프로그래밍 언어이다.
    • 정적 타입 체크를 위한 자체 컴파일러(tsc)를 가지고 있다. 이를 통해 컴파일 타임에 빌드 에러와 버그를 잡을 수 있다.
    • Create React App은 TypeScript를 별도의 설정 없이 사용할 수 있도록 지원해준다.
      • "npx create-react-app 프로젝트명 --template typescript" 명령어로 프로젝트를 생성한다.
      • 기존의 Create React App 프로젝트에 TypeScript 설정을 추가해주는 것도 가능하다. (참고)
    • TypeScript 사용 방법 (4단계)
      • 1st) TypeScript 패키지를 설치하여 프로젝트 의존성에 추가한다.
        • "npm install --save-dev typescript" 혹은 "yarn add --dev typescript" 명령어로 최신 버전의 TypeScript를 설치한다.
        • TypeScript를 설치하면 tsc 명령어를 실행할 수 있다.
        • 추후 터미널에서 TypeScript 컴파일러를 사용하기 위해 package.json 파일의 "scripts" 부분에 "build": "tsc"를 추가한다.
      • 2nd) TypeScript 컴파일러 옵션을 설정한다.
        • "npx tsc --init" 혹은 "yarn run tsc --init" 명령어로 TypeScript 설정 파일(tsconfig.json)을 생성한다.
        • 이 파일에 설정할 수 있는 다양한 종류의 옵션에 대해서는 여기를 참조하자.
        • 대표적인 옵션 두 가지는 rootDir와 outDir이다. 컴파일할 JavaScript 파일들은 rootDir(EX. src 디렉토리)에 두고, 컴파일 결과물들은 outDir(EX. build 디렉토리)에 저장된다. 이때 outDir 디렉토리는 .gitignore 파일에 추가하자.
        • 참고로, TypeScript React Starter는 시작하기에 좋은 규칙들을 정의해놓은 tsconfig.json 파일을 제공한다.
      • 3rd) TypeScript 문법의 사용을 위한 올바른 파일 확장자를 사용하고, 컴파일러를 실행한다.
        • React에서는 대부분의 컴포넌트를 .js 파일에 작성한다.
        • 그러나 TypeScript를 사용하려면 .ts 혹은 .tsx 확장자를 사용해야 한다. .tsx 확장자는 JSX 문법이 포함된 경우이다.
        • 이제 "npm run build" 혹은 "yarn build" 명령어로 TypeScript 컴파일러를 실행하여 컴파일한다.
      • 4th) TypeScript에서 사용하는 라이브러리의 정의를 추가한다.
        • 다른 Node.js 패키지들로부터의 에러나 힌트들을 보여주기 위해 컴파일러는 선언 파일들에 의존한다.
        • 선언 파일은 해당 라이브러리의 모든 타입 관련 정보들을 제공한다.
        • 이는 우리가 프로젝트에서 Node.js 기반의 JavaScript 라이브러리들을 사용할 수 있게 해준다.
        • 특정 라이브러리의 선언을 가져오는 방법
          • Bundled : 라이브러리 스스로가 자체 선언 파일을 가진 경우이다. 이는 우리가 단순히 해당 라이브러리를 설치하고 즉시 사용하기만 하면 되므로 매우 편리하다. 이 경우에 해당하는 라이브러리인지 확인하고 싶다면 해당 패키지 폴더에 찾아가서 index.d.ts 파일이 존재하는지 찾아보자. 또 어떤 라이브러리들은 package.json 파일의 typings 혹은 types 필드 아래에 정의해뒀을 수도 있다.
          • DefinitelyTyped : DefinitelyTyped는 자체 선언 파일을 가지고 있지 않은 라이브러리들을 위한 선언들의 거대한 저장소이다. 이는 Microsoft와 오픈 소스 기여자들에 의해 관리된다. 예를 들어 React 라이브러리는 자체 번들 파일을 가지고 있지 않기 때문에, 따로 DefinitelyTyped에서 @types/react를 설치하여 사용해야 한다.
          • Local Declarations : 어떤 라이브러리는 자체 선언 파일도 없고 DefinitelyTyped에도 설치 가능한 게 없을 수 있다. 이 경우에는 직접 지역 선언 파일(declaration.d.ts)을 생성해서 소스 디렉토리 루트에 두어야 한다. 

 

14. PropTypes를 사용한 타입 검사 (Typechecking With PropTypes)

 

PropTypes와 함께 하는 타입 확인 – React

A JavaScript library for building user interfaces

ko.reactjs.org

  • 컴포넌트에 전달되는 각각의 prop에 대해, 특정한 조건을 만족하도록 유효성 검사기를 지정하는 방법을 말한다.
  • 컴포넌트의 propTypes 프로퍼티에 각각의 prop이 어떤 조건을 만족해야 하는지를 나타내는 객체를 지정한다.
  • 특정 데이터가 명시된 조건을 만족하지 않는 경우, 콘솔 창에 경고문이 표시된다. 단, 성능 상 이유로 개발 모드에서만 동작한다. 
  • EX) PropTypes.number, PropTypes.node, PropTypes.element, PropTypes.instanceOf, PropTypes.oneOf 등 (참고)
  • React 15.5부터는 PropTypes를 react 라이브러리가 아닌 prop-types 라이브러리에서 제공하도록 바뀌었다.
  • 참고로 defaultProps 프로퍼티를 할당하면 각각의 prop에 초깃값을 설정해주는 것도 가능하다. 이때 propTypes의 타입(유효성) 검사는 defaultProps가 먼저 적용된 뒤에야 동작을 하도록 되어 있다.

 

15. 비제어 컴포넌트 (Uncontrolled Components)

 

비제어 컴포넌트 – React

A JavaScript library for building user interfaces

ko.reactjs.org

  • 대부분의 경우 폼을 구현할 때는 제어 컴포넌트가 권장된다.
  • 제어 컴포넌트에서는 폼의 값이 React에 의해 제어된다. 반면 비제어 컴포넌트에서는 폼의 값이 DOM 노드 자체에서 제어된다. 이 경우 Ref 메커니즘을 사용하면 해당 DOM 노드에 직접 접근하여 폼의 값을 읽을 수 있다.
  • React 폼 엘리먼트의 value 어트리뷰트는 렌더링 시 DOM 노드의 value 어트리뷰트로 대체된다. 그런데 비제어 컴포넌트를 사용하는 경우, React로 초기값을 설정해줄 수는 있어도 그 이후의 값은 제어하지 않는 편이 좋다. 따라서 React 폼 엘리먼트에는 value 어트리뷰트보다 defaultValue 어트리뷰트를 사용하는 것이 권장된다. 참고로 <input type="checkbox">, <input type="radio"> 엘리먼트는 defaultChecked 어트리뷰트를 지원한다.
  • 앞서 말했듯 <input type="file"> 엘리먼트는 읽기 전용이므로 비제어 컴포넌트에 해당한다.