Enjoy My Posts

리액트를 다루는 기술 정리

Posted on By Geunwon Lim

이 포스트에서는 velopert님의 리액트를 다루는 기술을 정리합니다. 정리하는대로 조금씩 업데이트할 예정입니다.

1. 리액트 시작

1.1 왜 리액트인가?

웹 페이지에서 업데이트하는 항목에 따라 어떤 부분을 찾아서 변경할지 규칙을 정하는 작업은 간단하지만, 애플리케이션 규모가 크면 상당히 복잡해지고 제대로 관리하지 않으면 성능도 떨어질 수 있다. 이를 개선하기 위해 어떤 데이터가 변할 때마다 어떤 변화를 줄지 고민하는 것이 아니라 기존 뷰를 날려버리고 처음부터 새로 렌덩하는 방식을 생각할 수 있다. 리액트가 이러한 것을 해준다.

1.2 리액트 이해

리액트는 오직 뷰만 신경쓰는 라이브러리다. 리액트는 초기 렌더링, 조화 과정을 거친다.

초기 렌더링

초기 렌더링에서 render 함수는 컴포넌트(특정 부분이 어떻게 생길지 정하는 선언체)가 어떻게 생겼는지 정의하는 역할을 한다. render 함수는 html 형식의 문자열을 반환하지 않고, 뷰가 어떻게 생겼고 어떻게 작동하는지에 대한 정보를 지닌 객체를 반환한다. 컴포넌트 내부에는 또 다른 컴포넌트들이 들어갈 수 있다. 이 때 render 함수를 실행하면 그 내부에 있는 컴포넌트들도 재귀적으로 렌더링한다. 최상위 컴포넌트의 렌더링 작업이 끝나면 니니고 있는 작업들을 사용하여 html 마크업을 만들고, 이룰 우리가 정하는 실제 페이지의 dom 요소 안에 주입한다.

조화 과정

컴포넌트에서 데이터에 변화가 있을 때 우리가 보기에는 변화에 따라 뷰가 변형되는 것처럼 보이지만 사실 새로운 요소로 갈아끼운다. 컴포넌트는 데이터를 업데이트했을 때 단순히 업데이트한 값을 수정하는 것이 아니라, 새로운 데이터를 가지고 render 함수를 또 다시 호출한다. 그러면 render 함수는 그 데이터를 지닌 뷰를 생성한다. 이 때 render 함수가 반환하는 결과를 곧바로 dom에 반영하지 않고, 이전 render 함수가 만들었던 컴포넌트 정보와 현재 render 함수가 만든 컴포넌트 정보를 비교한다. 비교 후 둘의 차이를 알아내 최소한의 연산으로 dom 트리를 업데이트한다. 즉, 루트 노드부터 시작하여 전체 컴포넌트를 처음부터 다시 렌더링하는 것처럼 보이지만, 사실 최적의 자원을 사용하여 이를 수행하는 것이다.

1.3 리액트의 특징

dom은 객체로 문서 구조를 표현하는 방법이다. dom은 약점이 있는데, 동적 ui에 최적화되어 있지 않다는 것이다. 규모가 큰 웹 애플리케이션에서 dom에 직접 접근하여 변화를 주다 보면 성능이슈가 조금씩 발생한다. dom 자체가 느린 것은 아니다. 단, 웹 브라우저 단에서 dom에 변화가 일어나면 웹 브라우저가 css를 다시 연산하고, 레이아웃으 ㄹ 구성하고, 페이지를 리페인트하게 되는데 이 과정에서 시간을 허비한다. 이를 해결하기 위해서는 dom을 최소한으로 조작하여 작업을 처리해야 한다. 리액트는 virtual dom 방식을 사용하여 dom 업데이트를 추상화함으로써 dom 처리 횟수를 최소화하고 효율적으로 진행한다.

virtual dom을 사용하면 실제 dom에 접근하여 조작하는 대신, 이를 추상화한 자바스크립트 객체를 구성하여 사용한다. 리액트에서 데이터가 변하여 웹 브라우저에 실제 dom을 업데이트할 때는 다음 세 절차를 거친다.

  1. 데이터를 업데이트하면 전체 ui를 virtual dom에 리렌더링한다.
  2. 이전 virtual dom에 있던 내용과 현재 내용을 비교한다.
  3. 바뀐 부분만 실제 dom에 적용한다.

2. JSX

프로젝트 생성 과정에서 node_modlues 디렉터리에 react 모듈이 설치된다. 그리고 import를 통해 리액트를 불러온다. 이렇게 모듈을 불러와 사용하는 것은 사실 원래 브라우저에는 없던 기능이다. 브라우저가 아닌 환경에서 자바스크립트를 실행할 수 있게 해주는 환경인 Node.js에서 지원하는 기능이다. 이러한 기능을 브라우저에서도 사용하기 위해 번들러를 사용한다. 번들은 묶는다는 뜻이고, 즉 파일을 묶듯이 연결하는 것이다. 대표적은 번들러로 웹팩이 있다. 번들러를 사용하면 import로 모듈을 불러왔을 때 불러온 모듈을 모두 합쳐서 하나의 파일을 생성해준다. 또 최적화 과정에서 여러 개의 파일로 분리될 수도 있다. 웹팩을 사용하면 svg파일과 css파일도 불러와 사용할 수 있는데, 이렇게 파일들을 불러오는 것은 웹팩의 로더라는 기능이 담당한다.

최신 자바스크립트로 작성된 코드를 변환하는 이유

ES5는 이전 버전의 자바스크립트를 의미한다. 최신 자바스크립트 문법을 ES5 형태로 변환하는 것은 구버전 웹 브 라우저와 호환하기 위해서다. JSX도 정식 자바스크립트 문법이 아니므로 ES5 형태의 코드로 변환해야 한다.

2.1 JSX란?

JSX로 작성된 코드는 브라우저에서 실행되기 전에 코드가 번들링되는 과정에서 바벨을 사용하여 일반 자바스크립트 형태의 코드로 변환된다. JSX를 사용하면 편하게 UI를 렌더링할 수 있다.

ES6의 const와 let

var는 scope가 함수 단위다. 이를 개선하는 것이 let과 const이다. let과 const는 scope가 함수 단위가 아니라 블록 단위이다. ES6에서 var을 사용할 일은 없다. let은 한번 선언한 후 값이 유동적으로 변할 수 있을 때만(예: for문) 사용하고, const는 한번 설정한 후 변할 일이 없는 값에 사용한다. 편하게 기본적으로 const, 해당 값을 바꿔야 할 때는 let을 사용한다.

3. 컴포넌트

컴포넌트의 기능은 단순한 템플릿 이상이다. 데이터가 주어졌을 때 이에 맞춰 ui를 만들어주는 것은 물론이고, 라이프사이클 api를 사용하여 컴포넌트가 화면에서 나타날 때, 사라질 때, 변화가 일어날 때 주어진 작업들을 처리할 수 있으며, 임의 메서드를 만들어 특별한 기능을 붙여줄 수 있다.

컴포넌트를 선언하는 방식은 두가지다. 하나는 함수형 컴포넌트고, 또 다른 하나는 클래스형 컴포넌트다. 클래스형 컴포넌트와 함수형 컴포넌트의 차이점은 클래스형 컴포넌트의 경우 이후 배울 state 기능 및 라이프사이클 기능을 상요할 수 있다는 것과 임의 메서드를 정의할 수 있다는 것이다. 클래스형 컴포넌트에서는 render 함수가 꼭 있어야 하고, 그 안에서 보여주어야 할 jsx를 반환해야 한다.

함수형 컴포넌트의 장점을 나열해보면 다음과 같다. 우선 클래스형 컴포넌트보다 선언하기 훨씬 편하다. 메모리 자원도 클래스형 컴포넌트보다 덜 사용한다. 또한 프로젝트를 완성하여 빌드한 후 배포할 때도 함수형 컴포넌트를 사용하는 것이 결과물의 크기가 더 작다. 함수형 컴포넌트의 주요 단점은 state와 라이프사이클 api의 사용이 불가능하다는 점이다. 이 단점은 hooks라는 기능이 도입되면서 해결되었다.

function()과 () => 는 서로 가리키는 this가 다르다. 일반 함수는 자신이 종속된 객체를 this로 가리키고, 화살표 함수는 자신이 종속된 인스턴스를 가리킨다.

export는 다른 파일에서 파일을 import할 때 선언한 클래스를 불러오도록 한다.

props

props는 properties를 줄인 표현으로 컴포넌트 속성을 설정할 때 사용되는 요소다. props 값은 부모 컴포넌트에서 설정할 수 있다. defaultProps를 설정하면 props의 기본값을 설정할 수 있다.

children을 사용하면 태그 사이의 내용을 보여줄 수 있다.

컴포넌트의 필수 props를 지정하거나 props의 타입을 지정할 땐 propTypes를 사용한다. isRequired를 사용하면 필수 propTypes를 설정할 수 있다.

state

리액트에서 state는 컴포넌트 내부에서 바뀔 수 있는 값을 의미한다. props는 컴포넌트가 사용되는 과정에서 부모 컴포넌트가 설정하는 값이며, 컴포넌트 자신은 해당 props를 읽기 전용으로만 사용할 수 있다. props를 바꾸려면 부모 컴포넌트에서 바꾸어 주어야 한다. 리액트에는 두 종류의 state가 있다. 하나는 클래스형 컴포넌트가 지니고 있는 state이고 다른 하나는 함수형 컴포넌트에서 useState라는 함수를 통해 사용하는 state이다.

클래스형 컴포넌트든 함수형 컴포넌트든 state를 사용할 때는 주의해야 할 사항이 있다. state 값을 바꿔야 할 때는 setState 혹은 useState를 통해 전달받은 세터 함수를 사용해야 한다.

props와 states는 둘 다 컴포넌트에서 사용하거나 렌더링할 데이터를 담고 있으므로 비슷해 보일 수 있지만, 그 역할은 다르다. props는 부모 컴포넌트가 설정하고, state는 컴포넌트 자체적으로 지닌 값으로 컴포넌트 내부에서 값을 업데이트할 수 있다.

4. 이벤트 핸들링

4.1 이벤트를 사용할 때 주의사항

  1. 이벤트 이름은 카멜 표기법으로 작성한다.
  2. 이벤트에 실행할 자바스크립트 코드를 전달하는 것이 아니라, 함수 형태의 값을 전달한다. HTML에서 이벤트를 설정할 때는 큰따옴표 안에 실행할 코드를 넣었지만, 리액트에서는 함수 형태의 객체를 전달한다.
  3. DOM 요소에만 이벤트를 설정할 수 있다. 직접 만든 컴포넌트에는 이벤트를 자체적으로 설정할 수 없다. 전달받은 props를 컴포넌트 내부의 DOM 이벤트로 설정할 수는 있다.

4.2 이벤트 종류

  • Clipboard
  • Composition
  • Keyboard
  • Focus
  • Form
  • Mouse
  • Selection
  • Touch
  • UI
  • Wheel
  • Media
  • Image
  • Animation
  • Transition

참고: https://facebook.github.io/react/docs/events.html

4.3 임의 메서드 만들기

이벤트에 실행할 자바스크립트 코드를 전달하는 것이 아니라, 함수 형태의 값을 전달해야 한다. 이 방법 말고 함수를 미리 준비하여 전달할 수도 있다. 이 때 주의할 점이 있다. 함수가 호출될 때 this는 호출부에 따라 결정되므로, 클래스의 임의 메서드가 특정 HTML 요소의 이벤트로 등록되는 과정에서 메서드와 this의 관계가 끊어져 버린다. 이 때문에 임의 메서드가 이벤트로 등록되어도 this를 컴포넌트 자신으로 제대로 가리키기 위해서는 메서드를 this와 바인딩하는 작업이 필요하다. 만약 바인딩하지 않는 경우라면 this가 undefined를 가리키게 된다.

5. ref: DOM에 이름 달기

HTML에서 id를 사용하여 DOM에 이름을 다는 것처럼 리액트 프로젝트 내부에서 DOM에 이름을 다는 방법이 있다. 바로 ref 개념이다. 리액트 컴포넌트 안에서도 id를 사용할 수 있긴 하지만, id는 유일해야 하는데 중복 id를 가진 DOM이 여러 개 생길 수 있어 잘못 사용될 수 있다. ref는 전역적으로 작동하지 않고 컴포넌트 내부에서만 작동하기 때문에 이런 문제가 생기지 않는다.

5.1 ref는 어떤 상황에서 사용해야 할까?

특정 DOM에 작업을 해야할 때 ref를 사용한다는 것은 이미 알고 있는데, 대체 ‘어떤’ 작업을 할 때 ref를 사용해야 할까? 정답은 ‘DOM을 꼭 직접적으로 건드려야 할 때’이다.

5.2 ref 사용

5.2.1 콜백 함수를 통한 ref 설정

ref를 만드는 가장 기본적인 방법은 콜백 함수를 사용하는 것이다. ref를 달고자 하는 요소에 ref라는 콜백 함수를 props로 전달해 주면된다.

컴포넌트 내부에서 DOM에 직접 접근해야 할 때는 ref를 사용한다. 먼저 ref를 사용하지 않고도 원하는 기능을 구현할 수 있는지 반드시 고려하고 사용한다. 서로 다른 컴포넌트끼리 데이터를 교류할 때 ref를 사용한다면 이는 잘못 사용된 것이다. 앱 규모가 커지면 마치 스파게티처럼 구조가 꼬여 버려서 유지 보수가 불가능해지기 때문이다. 컴포넌트끼리 데이터를 교류할 때는 언제나 데이터를 부모, 자식 흐름으로 교류해야 한다.

6. 컴포넌트 반복

6.1 자바스크립트 배열의 map() 함수

자바스크립트 배열 객체의 내장 함수인 map 함수를 사용하여 반복되는 컴포넌트를 렌더링할 수 있다.

arr.map(callback, [thisArg])

  • callback: 새로운 배열의 요소를 생성하는 함수로 파라미터는 다음 세 가지이다.
    • currentValue: 현재 처리하고 있는 요소
    • index: 현재 처리하고 있는 요소의 index 값
    • array: 현재 처리하고 있는 원본 배열
  • thisArg(선택 항목): callback 함수 내부에서 사용할 this 레퍼런스

6.2 Key

리액트에서 Key는 컴포넌트 배열을 렌더링했을 때 어떤 원소에 변동이 있었는지 알아내려고 사용한다. key가 없을 때는 Virtual DOM을 비교하는 과정에서 리스트를 순차적으로 비교하면서 변화를 감지한다. 하지만 key가 있다면 이 값을 사용하여 어떤 변화가 일어났는지 더욱 빠르게 알아낼 수 있다.

key 값을 설정할 때는 map 함수의 인자로 전달되는 함수 내부에서 컴포넌트 props를 설정하듯이 설정하면 된다. key 값은 언제나 유일해야 한다. 따라서 데이터가 가진 고윳값을 key 값으로 설정해야 한다.

push와 concat은 push 함수는 기존 배열 자체를 변경해주는 반면, concat은 새로운 배열을 만들어 준다는 차이점이 있다. 이렇게 불변성을 유지해야 리액트 컴포넌트의 성능을 최적화할 수 있다.

컴포넌트 배열을 렌더링할 때는 key 값 설정에 항상 주의해야 한다. 또 key 값은 언제나 유일해야 한다. key 값이 중복된다면 렌더링 과정에서 오류가 발생한다.

상태 안에서 배열을 변형할 때는 배열에 직접 접근하여 수정하는 것이 아니라 concat, filter 등의 배열 내장 함수를 사용하여 새로운 배열을 만든 후 이를 새로운 상태로 설정해 주어야 한다.

7. 컴포넌트의 라이프사이클 메서드

모든 리액트 컴포넌트에는 라이프사이클이 존재한다. 컴포넌트의 수명은 페이지에 렌더링되기 전인 준비 과정에서 시작하여 페이지에서 사라질 때 끝난다.

리액트 프로젝트를 진행하다 보면 가끔 컴포넌트를 처음으로 렌더링할 때 어떤 작업을 처리해야 하거나 컴포넌트를 업데이트하기 전후로 어떤 작업을 처리해야 할 수도 있고, 또 불필요한 업데이트를 방지해야 할 수도 있다. 라이프사이클 메서드는 클래스형 컴포넌트에서만 사용할 수 있다.

7.1 라이프사이클 메서드의 이해

Will 접두사가 붙은 메서드는 어떤 작업을 작동하기 전에 실행되는 메서드이고, Did 접두사가 붙은 메서드는 어떤 작업을 작동한 후에 실행되는 메서드이다.

라이프사이클은 마운트, 업데이트, 언마운트 카테고리로 나눈다.

마운트

DOM이 생성되고 웹 브라우저상에 나타나는 것을 마운트라고 한다. 이 때 호출되는 메서드는 다음과 같다.

  • constructor: 컴포넌트를 새로 만들 때마다 호출되는 클래스 생성자 메서드이다.

  • getDerivedStateFromProps: props에 있는 값을 state에 넣을 때 사용하는 메서드이다.

  • render: 우리가 준비한 UI를 렌더링하는 메서드이다.

  • componentDidMount: 컴포넌트가 웹 브라우저상에 나타난 후 호출하는 메서드이다.

업데이트

컴포넌트는 다음과 같은 총 네 가지 경우에 업데이트한다.

  1. props가 바뀔 때
  2. state가 바뀔 때
  3. 부모 컴포넌트가 리렌더링될 때
  4. this.forceUpdate로 강제로 렌더링을 트리거할 때

이 때 호출되는 메서드는 다음과 같다.

  • getDerivedStateFromProps: 이 메서드는 마운트 과정에서도 호출되며, 업데이트가 시작하기 전에도 호출된다. props의 변화에 따라 state 값에도 변화를 주고 싶을 때 사용한다.
  • shouldComponentUPdate: 컴포넌트가 리렌더링을 해야 할지 말아야 할지를 결정하는 메서드이다. 이 메서드는 true 혹은 false를 반환해야 하며, true를 반환하면 다음 라이프사이클 메서드를 계속 실행하고, false를 반환하면 작업을 중지한다. 즉 컴포넌트가 리렌더링되지 않는다. 만약 특정 함수에서 this.forceUpdate() 함수를 호출한다면 이 과정을 생략하고 바로 render 함수를 호출한다.
  • render: 컴포넌트를 리렌더링한다.
  • getSnapshipBeforeUpdate: 컴포넌트 변화를 DOM에 반영하기 바로 직전에 호출하는 메서드이다.
  • componentDidUpdate: 컴포넌트의 업데이트 작업이 끝난 후 호출하는 메서드이다.

언마운트

컴포넌트를 DOM에서 제거하는 것을 말한다.

componentWillUnmount: 컴포넌트가 웹 브라우저상에서 사라지기 전에 호출하는 메서드이다.

7.2 라이프사이클 메서드 살펴보기

7.2.1 render() 메서드

this.props와 this.state에 접근할 수 있고, 리액트 요소를 반환한다. 요소는 div 같은 태그가 될 수도 있고, 따로 선언한 컴포넌트가 될 수도 있다. 아무것도 보여주고 싶지 않다면 null 값이나 false 값을 반환하라.

이 메서드 안에서는 이벤트 설정이 아닌 곳에서 setState를 사용하면 안되고, 브라우저의 DOM에 접근해서도 안된다. DOM 정보를 가져오거나 state에 변화를 줄 때는 componentDidMount에서 처리해야 한다.

7.2.2 constructor 메서드

컴포넌트의 생성자 메서드로 컴포넌트를 만들 때 처음으로 실행된다. 이 메서드에서는 초기 state를 정할 수 있다.

7.2.3 getDerivedStateFromProps 메서드

props로 받아 온 값을 state에 동기화시키는 용도로 사용하고, 컴포넌트가 마운트될 때와 업데이트될 때 호출된다.

7.2.4 componentDidMount 메서드

컴포넌트를 만들고 첫 렌더링을 다 마친 후 실행된다. 이 안에서 다른 자바스크립트 라이브러리 또는 프레임워크의 함수를 호출하거나 이벤트 등록, setTimeout, setInterval, 네트워크 요청 같은 비동기 작업을 처리하면 된다.

7.2.5 shouldComponentUpdate 메서드

props 또는 state를 변경했을 때 리렌더링을 시작할지 여부를 지정하는 메서드이다. 이 메서드에서는 반드시 true, flase 값을 반환해야 한다. 이 메서드 안에서 현재 props와 state는 this.props와 this.state로 접근하고, 새로 설정될 props 또는 state는 nextProps와 nextState로 접근할 수 있다.

7.2.6 getSnapshopBeforeUpdate 메서드

render에서 만들어진 결과물이 브라우저에 실제로 반영되기 직전에 호출된다. 이 메서드에서 반환하는 값은 componentDidUpdate에서 세 번째 파라미터인 snapshot 값으로 전달받을 수 있는데, 주로 업데이트하기 직전의 값을 참고할 일이 있을 때 활용된다(예: 스크롤바 위치 유지).

7.2.7 componentDidUpdate 메서드

리렌더링을 완료한 후 실행된다. 업데이트가 끝난 직후이므로 DOM 관련 처리를 해도 무방하다. 여기서는 prevProps, prevState를 사용하여 컴포넌트가 이전에 가졌던 데이터에 접근할 수 있다. 또 getSnapshipBeforeUpdate에서 반환한 값이 있다면 여기서 snapshop 값을 전달받을 수 있다.

7.2.8 componentWillUnmount 메서드

컴포넌트를 DOM에서 제거할 때 실행된다. componentDidMount에서 등록한 이벤트,타이머, 직접 생성한 DOM이 있다면 여기서 제거 작업을 해야 한다.

7.2.9 componentDidCatch 메서드

컴포넌트 렌더링 도중에 에러가 발생했을 때 애플리케이션이 먹통이 되지 않고 오류 UI를 보여줄 수 있게 해준다.

라이프사이클 메서드는 컴포넌트 상태에 변화가 있을 때마다 실행하는 메서드이다. 이 메서드들은 서드파티 라이브러리를 사용하거나 DOM을 직접 건드려야 하는 상황에서 유용하다.

8. Hooks

8.1 useState

함수형 컴포넌트에서도 가변적인 상태를 지닐 수 있게 해준다. useState()가 호출되면 배열을 반환한다. 배열의 첫 번째 원소는 상태 값, 두 번째 원소는 상태를 설정하는 함수이다. 함수에 파라미터를 넣어서 호출하면 전달받은 파라미터로 값이 바뀌고 컴포넌트가 정상적으로 리렌더링된다.

하나의 useState 함수는 하나의 상태 값만 관리할 수 있다. 컴포넌트에서 관리해야 할 상태가 여러 개라면 useState를 여러 번 사용하면 된다.

8.2 useEffect

리액트 컴포넌트가 렌더링될 때마다 특정 작업을 수행하도록 설정할 수 있는 Hook이다. 클래스형 컴포넌트의 componentDidMount와 componentDidUpdate를 합친 형태로 보아도 무방하다.

useEffect에서 설정한 함수를 컴포넌트가 화면에 맨 처음 렌더링될 때만 실행하고, 업데이트될 때는 실행하지 않으려면 함수의 두 번째 파라미터로 비어 있는 배열을 넣어 주면 된다.

특정 값이 업데이트될 때만 실행하고 싶다면, useEffect의 두 번째 파라미터로 전달되는 배열 안에 검사하고 싶은 값을 넣어 주면 된다.

useEffect는 기본적으로 렌더링되고 난 직후마다 실행되고, 두 번째 파라미터 배열에 무엇을 넣는지에 따라 실행되는 조건이 달라진다.

컴포넌트라 언마운트되기 전이나 업데이트되기 직전에 어떠한 작업을 수행하고 싶다면 useEffect에서 cleanup 함수를 반환해주어야 한다.

8.3 useReducer

useReducer는 useState보다 더 다양한 컴포넌트 상황에 따라 다양한 상태를 다른 값으로 업데이트해주고 싶을 때 사용하는 Hook이다. 리듀서는 현재 상태, 그리고 업데이트를 위해 필요한 정보를 담은 액션 값을 전달받아 새로운 상태를 반환하는 함수이다. 리듀서 함수에서 새로운 상태를 만들 때는 반드시 불변성을 지켜줘야 한다.

useReducer의 리턴 값은 state와 dispatch이다. state는 현재 가리키고 있는 상태고 dispatch는 액션을 발생시키는 함수다.

useReducer를 사용했을 때의 가장 큰 장점은 컴포넌트 업데이트 로직을 컴포넌트 바깥으로 빼낼 수 있다는 것이다.

기존에는 인풋이 여러개일 때 useState를 여러번 사용했는데, useReducer를 사용하면 기존에 클래스형 컴포넌트에서 input 태그에 name 값을 할당하고 e.target.name을 참조하여 setState를 해 준 것과 유사한 방식으로 작업을 처리할 수 있다.

8.4 useMemo

useMemo를 사용하면 함수형 컴포넌트 내부에서 발생하는 연산을 최적화할 수 있다.

8.5 useCallback

useCallback은 useMemo처럼 주로 렌더링 성능을 최적화해야 하는 상황에서 사용한다. 이 Hook을 사용하면 이벤트 핸들러 함수를 필요할 때만 생성할 수 있다. useCallback의 첫 번째 파라미터에는 생성하고 싶은 함수를 넣고, 두 번째 파라미터에는 배열을 넣으면 된다. 이 배열에는 어떤 값이 바뀌었을 때 함수를 새로 생성해야 하는지 명시해야 한다. 함수 내부에서 상태 값에 의존해야 할 때는 그 값을 반드시 두 번째 파라미터 안에 포함시켜줘야 한다.

useCallback은 결국 useMemo로 함수를 반환하는 상황에서 더 편하게 사용할 수 있는 Hook이다. 숫자, 문자열, 객체처럼 일반 값을 재사용하려면 useMemo를 사용하고, 함수를 재사용하려면 useCallback을 사용하라.

8.6 useRef

useRef Hook은 함수형 컴포넌트에서 ref를 쉽게 사용할 수 있도록 해준다.

추가로 컴포넌트 로컬 변수를 사용해야 할 때도 useRef를 활용할 수 있다. 여기서 로컬 변수란 렌더링과 상관없이 바뀔 수 있는 값을 의미한다.

8.7 커스텀 Hooks 만들기

컴포넌트에서 비슷한 기능을 공유할 경우, 나만의 Hook으로 작성하여 로직을 재사용할 수 있다.

8.8 다른 Hooks

https://nikgraf.github.io/react-hooks/ https://github.com/rehooks/awesome-react-hooks

9. 컴포넌트 스타일링

9.1 일반 css

css 클래스를 중복되지 않게 해야한다. 그러기 위해 특별한 규칙을 사용하거나 css selector를 활용해야 한다.

9.2 Sass 사용

Sass는 css 전처리기로 복잡한 작업을 쉽게 할 수 있도록 해주고 스타일 코드의 재활용성을 높여 줄 뿐 아니라 코드의 가독성을 높여준다.

여러 파일엫서 사용될 수 있는 Sass 변수 및 믹스인은 다른 파일로 따로 분리하여 작성한 뒤 필요한 곳에서 쉽게 불러와 사용할 수 있다.

~를 활용하면 node_modules에서 라이브러리 디렉터리를 탐지하여 스타일을 불러올 수 있다.

9.3 CSS Module

css module은 css를 불러와서 사용할 때 클래스 이름을 고유한 값, 즉 [파일 이름]_[클래스 이름]__[해시값] 형태로 자동으로 만들어서 컴포넌트 스타일 클래스 이름이 중첩되는 현상을 방지해주는 기술이다. .module.css 확장자로 파일을 저장하기만 하면 css module이 적용된다.

9.4 styled-components

컴포넌트 스타일링의 또 다른 패러다임은 자바스크립트 파일 안에 스타일을 선언하는 방식이다. 이 방식을 ‘CSS-in-JS’라고 부른다. styled-components를 사용하면 자바스크립트 파일 하나에 스타일까지 작성할 수 있기 때문에 .css 또는 .scss 확장자를 가진 스타일 파일을 따로 만들지 않아도 된다는 이점이 있다. styled-components와 일반 classNames를 사용하는 CSS/Sass를 비교할 때, 가장 큰 장점은 props 값으로 전달해주는 값을 쉽게 스타일에 적용할 수 있다는 것이다.

10. 일정 관리 웹 애플리케이션 만들기

display 속성 중 flex: Flexbox Froggy

11. 컴포넌트 성능 최적화

컴포넌트는 다음과 같은 상황에서 리렌더링이 발생한다.

  1. 자신이 전달받은 props가 변경될 때
  2. 자신의 state가 바뀔 때
  3. 부모 컴포넌트가 리렌더링될 때
  4. forceUpdate 함수가 실행될 때

리렌더링이 불필요할 때는 리렌더링을 방지해줘야 한다.

11.1 React.momo를 사용하여 컴포넌트 성능 최적화

React.memo를 활용하여 컴포넌트의 props가 바뀌지 않았다면 리렌더링하지 않도록 설정하여 함수형 컴포넌트의 리렌더링 성능을 최적화해 줄 수 있다. 이를 활ㄹ용하면 컴포넌트는 props가 바뀌지 않으면 리렌더링을 하지 않는다.

11.2 불변성의 중요성

기존 데이터를 수정할 때 직접 수정하지 않고 새로운 데이터를 만들어 필요한 부분을 교체한다. 새로운 데이터이기 때문에 React.memo를 사용했을 때 props가 바뀌었는지 혹은 바뀌지 않았는지를 알아내서 리렌더링 성능을 최적화할 수 있다. 불변성이 지켜지지 않으면 객체 내부의 값이 새로워져도 바뀐 것을 감지하지 못한다.

12. immer를 사용하여 더 쉽게 불변성 유지하기

객체의 구조가 엄청 깊어지면 불변성을 유지하면서 이를 업데이트하는 것이 매우 힘들다. 전개 연산자와 배열 내장 함수를 사용하여 불변성을 유지할 수 있지만, 상태가 복잡해진다면 귀찮은 작업이 될 수 있다.

immer 라이브러리의 핵심은 ‘불변성에 신경 쓰지 않는 것처럼 코드를 작성하되 불변성 관리는 제대로 해주는 것’이다. 단순히 깊은 곳에 위치하는 값을 바꾸는 것 외에 배열을 처리할 때도 쉽고 편리하다.

immer는 불변성을 유지하는 코드가 복잡할 때만 사용해도 충분하다.

13. 리액트 라우터로 SPA 개발하기

13.1 SPA란?

SPA는 한 개의 페이지로 이루어진 애플리케이션이라는 의미다. 기존에는 사용자가 다른 페이지로 이동할 때마다 새로운 html을 받아오고, 페이지를 로딩할 때마다 서버에서 리소스를 전달받아 해석한 뒤 화면에 보여 주었다. 사용자에게 보이는 화면은 서버 측에서 준비했다. 사전에 html 파일을 만들어 제공하거나, 데이터에 따라 유동적인 html을 생성해주는 템플릿 엔진을 사용하기도 했다.

새로운 화면을 보여줘야 할 때마다 서버 측에서 모든 뷰를 준비한다면 성능상의 문제가 발생할 수 있다. 트래픽이 너무 많이 나올 수도 있고, 사용자가 몰려 서버에 높은 부하가 걸릴 수도 있다. 속도와 트래픽 측면에서는 캐싱과 압ㅈ축을 해서 서비스를 제공하면 어느 정도 최적화될 수 있지만, 사용자와의 인터랙션이 자주 발생하는 모던 웹 애플리케이션에는 적당하지 않을 수도 있다. 애플리케이션 내에서 화면 전환이 일어날 때마다 html을 계속 서버에 새로 요청하면 사용자의 인터페이스에서 사용하고 있던 상태를 유지하는 것도 번거롭고, 바뀌지 않는 부분까지 새로 불러와서 보여줘야 하기 때문에 불필요한 로딩이 있어 비효율적이다.

그래서 리액트 같은 기술을 사용하여 뷰 렌더링을 사용자의 브라우저가 담당하도록 하고, 앱 애플리케이션을 브라우저에 불러와서 실행시킨 후 사용자와의 인터랙션이 발생하면 필요한 부분만 자바스크립트를 사용해 업데이트해 준다.

싱글 페이지라고 해서 화면이 한 종류인 것은 아니다. SPA의 경ㅇ우 서버에서 사용자에게 제공하는 페이지는 한 종류이지만, 해당 페이지에서 로딩된 자바스크립트와 현재 사용자 브라우저의 주소 상태에 따라 다양한 화면을 보여줄 수 있다.

다른 주소에 다른 화면을 보여주는 것을 라우팅이라고 한다.

13.1.1 SPA의 단점

SPA의 단점은 앱의 규모가 커지면 자바스크립트 파일이 너무 커진다는 것이다. 페이지 로딩 시 사용자가 실제로 방문하지 않을 수도 있는 페이지의 스크립트도 불러오기 때문이다. 하지만 코드 스플리팅을 사용하면 라우터별로 파일들을 나누어 트래픽과 로딩 속도를 개선할 수 있다.

13.2 라우팅 적용해보기

react-router-dom에 내장되어 있는 BrowserRouter라는 컴포넌트는 웹 애플리케이션에 HTML5의 History API를 사용해 페이지를 새로고침하지 않고도 주소를 변경하고 현재 주소에 관련된 정보를 props로 쉽게 조회하거나 사용할 수 있도록 해준다.

Link 컴포넌트는 클릭하면 다른 주소로 이동시켜 주는 컴포넌트이다. 이랍ㄴ 웹 애플리케이션에서는 a 태그를 사용해 페이지를 전환한다. 리액트 라우터를 사용할 땐 이 태그를 직접 사용하면 안 된다. a 태그는 페이지를 전환하는 과정에서 페이지를 새로 불러오기 때문에 애플리케이션이 들고 있던 상태들을 모두 날려 버리기 때문이다. 렌더링된 컴포넌트들도 모두 사라지고 다시 처음부터 렌더링하게 된다. Link 컴포넌트를 사용하여 페이지를 전환하면 페이지를 새로 불러오지 않고 애플리케이션은 그대로 유지한 상태에서 HTML5 History API를 사용하여 페이지의 주소만 변경해준다. Link 컴포넌트 자체는 a태그로 이뤄져 있지만, 페이지 전환을 방지하는 기능이 내장되어 있다.

URL 파라미터를 사용할 때는 라우트로 사용되는 컴포넌트에서 받아 오는 match라는 객체 안의 params 값을 참조한다. match 객체 안에는 현재 컴포넌트가 어떤 경로 규칙에 의해 보이는지에 대한 정보가 들어 있다.

13.3 서브 라우트

서브 라우트는 라우트 내부에 또 라우트를 정의하는 것을 의미한다. 그냥 라우트로 사용되고 있는 컴포넌트의 내부에 Route 컴포넌트를 또 사용하면 된다.

13.4 리액트 라우터 부가기능

13.4.1 history

history 객체는 라우트로 사용된 컴포넌트에 match, location과 함께 전달되는 props 중 하나로, 이 객체를 통해 컴포넌트 내에 구현하는 메서드에서 라우터 API를 호출할 수 있다. 예를 들어 특정 버튼을 눌렀을 때 뒤로 가거나, 로그인 후 화면으 ㄹ 전환하거나, 다른 페이지로 이탈하는 것을 방지해야 할 때 history를 활용한다.

13.4.2 withRouter

withRouter 함수는 라우트로 사용된 컴포넌트가 아니어도 match, location, history 객체를 접근할 수 있게 해준다.

13.4.3 Switch

Switch 컴포넌트는 여러 Route를 감싸서 그중 일치하는 단 하나의 라우트만을 렌더링시켜준다. Swith를 사용하면 모든 규칙과 일치파지 않을 때 보여 줄 Not Found 페이지도 구현할 수 있다.

현재 경로와 Link에서 사용하는 경로가 일치하는 경우 특정 스타일 혹은 CSS 클래스를 적용할 수 있는 컴포넌트이다.

NavLink에서 링크가 활성화되었을 때의 스타일을 적용할 때는 activeStyle 값을, CSS 클래스를 적용할 때는 activeClassName 값을 props로 넣어주면 된다.

14. 외부 API를 연동하여 뉴스 뷰어 만들기

14.1 비동기 작업의 이해

서버의 API를 사용해야 할 때는 네트워크 송수신 과정에서 시간이 걸리기 때문에 작업이 즉시 처리되는 것이 아니라, 응답을 받을 때까지 기다렸다가 전달받은 응답 데이터를 처리한다. 이 과정에서 해당 작업을 비동기적으로 처리하게 된다.

만약 작업을 동기적으로 처리한다면 요청이 끝날 때까지 기다리는 동안 중지 상태가 되기 때문에 다른 작업을 할 수 없다. 그리고 요청이 끝나야 비로소 그다음 예정된 작업을 할 수 있다. 하지만 이를 비동기적으로 처리한다면 웹 애플리케이션이 멈추지 않기 때문에 동시에 여러 가지 요청을 처리할 수도 있고, 기다리는 과정에서 다른 함수도 호출할 수 있다.

자바스크립트에서 비동기 작업을 할 때 가장 흔히 사용하는 방법은 콜백 함수를 사용하는 것이다.

14.1.2 Promise

Promise를 활용하면 여러 작업을 연달아 처리한다고 해서 함수를 여러 번 감싸는 것이 아니라, .then을 사용하여 그다음 작업을 설정하기 때문에 콜백 지옥이 형성되지 않는다.

14.1.3 async/await

async, await를 사용하면 Promise가 끝날 때까지 기다리고, 결과 값을 특정 변수에 담을 수 있다.

14.2 axios로 API 호출해서 데이터 받아오기

axios는 현재 가장 많이 사용되고 있는 자바스크립트 HTTP 클라이언트이다. 이 라이브러리의 특징은 HTTP 요청을 Promise 기반으로 처리한다는 점이다.

리액트 컴포넌트에서 API를 연동하여 개발할 때 절대 잊지 말아야 할 유의 사항은 useEffect에 등록하는 함수는 async로 작성하면 안 된다는 점이다. 그 대신 함수 내부에 async 함수를 따로 만들어줘야 한다.

15. Context API

Context API는 리액트에서 전역적으로 사용할 데이터가 있을 때 유용한 기능이다. 예를 들어 로그인 저보, 애플리케이션 환경 설정, 테마 등 말이다.

일반적으로 컴포넌트 여기저기서 필요한 데이터가 있을 때 주로 최상위 컴포넌트인 App의 state에 넣어서 관리한다. 리액트가 컴포넌트 간에 데이터를 props로 전달하기 때문이다. 그런데 이런 방식으로 할 때 복잡해져서(예시있음) 유지보수하기 어려워진다.

Provider를 사용할 땐 value 값을 명시해줘야 제대로 작동한다.

기존에는 컴포넌트 간에 상태를 교류해야 할 때 무조건 부모 -> 자식 흐름으로 props를 통해 전달해줬다. 이제 Context API를 통해 더욱 쉽게 상태를 교류할 수 있게 됐다. 프로젝트의 컴포넌트 구조가 간단하고 다루는 상태의 종류가 그다지 많지 않다면 굳이 Context를 사용할 필요는 없다. 하지만 전역적으로 여기저기서 사용되는 상태가 있고 컴포넌트의 개수가 많은 상황이라면, Context API를 사용하는 걸 권한다.

16. 리덕스 라이브러리 이해하기

리덕스를 사용하면 컴포넌트의 상태 업데이트 관련 로직을 다른 파일로 분리시켜 효율적으로 관리할 수 있다. 또한 컴포넌트끼리 똑같은 상태를 공유해야 할 때도 여러 컴포넌트를 거치지 않고 손쉽게 상태 값을 전달하거나 업데이트할 수 있다.

단순히 전역 상태 관리만 한다면 Context API를 사용하는 것만으로도 충분하다. 하지만 리덕스를 사용하면 더욱 체계적으로 관리할 수 있기 때문에 좋다.

16.1 개념 미리 정하기

1.6.1.1 액션

상태에 어떤 변화가 필요하면 액션이란 것이 발생한다. 액션 객체는 type 필드를 반드시 가지고 있어야 한다. 이 값을 액션의 이름이라고 생각하면 된다. 그 이외의 값은 작성자 마음대로 넣을 수 있다.

액션 생성 함수는 액션 객체를 만들어주는 함수다.

16.1.2 리듀서

액션을 만들어 발생시키면 리듀서가 현재 상태와 전달받은 액션 객체를 파라미터로 받아온다. 그리고 두 값을 참고하여 새로운 상태를 만들어 반환해준다.

16.1.3 스토어

한 개의 프로젝트는 하나의 스토어만 가질 수 있다. 스토어 안에는 현재 애플리케이션 상태와 리듀서가 들어있고 그 외에도 몇 가지 중요한 내장 함수를 지닌다.

16.1.4 디스패치

스토어의 내장 함수 중 하나다. ‘액션을 발생시키는 것’이라고 이해하면 된다.

16.1.5 구독

스토어의 내장 함수 중 하나. subscribe 함수 안에 리스너 함수를 파라미터로 넣어 호출해주면 리스너 함수가 액션이 디스패치되어 상태가 업데이트될 때마다 호출된다.

16.2 리덕스의 세 가지 규칙

  1. 단일 스토어
  2. 읽기 전용 상태: 리덕스의 상태는 읽기전용이고, 상태를 업데이트할 때 기존의 객체는 건드리지 않고 새로운 객체를 생성해줘야 한다. 리덕스에서 불변성을 유지해야 하는 이유는 내부적으로 데이터가 변경되는 것을 감지하기 위해 얕은 비교 검사를 하기 때문이다.
  3. 리듀서는 순수한 함수: 순수한 함수는 다음 조건을 만족한다. (1) 리듀서 함수는 이전 상태와 액션 객체를 파라미터로 받습니다. (2) 파라미터 외의 값에는 의존하면 안 된다. (3) 이전 상태는 절대로 건드리지 않고, 변화를 준 새로운 상태 객체를 만들어 반환한다. (4) 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과 값을 반환해야 한다.

17. 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기

리액트 애플리케이션에서 리덕스를 사용하면, 상태 업데이트에 관한 로직을 모듈로 따로 분리하여 컴포넌트 파일과 별개로 관리할 수 있으므로 코드를 유지 보수하는 데 도움이 된다. 또한 여러 컴포넌트에서 동일한 상태를 공유해야 할 때 유용하고, 실제 업데이트가 필요한 컴포넌트만 리렌더링되도록 쉽게 최족화해 줄 수 있다.

17.1 UI 준비하기

리덕스를 사용할 때 가장 많이 사용하는 패턴은 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트를 분리하는 것이다. 프레젠테이셔널 컴포넌트란 주로 상태 관리가 이뤄지지 않고, 그저 props를 받아 와서 화면에 UI를 보여 주기만 하는 컴포넌트를 말한다. 이와 달리 컨테이너 컴포넌트는 리덕스와 연동되어 있는 컴포넌트로, 리덕스로부터 상태를 받아 오기도 하고 리덕스 스토어에 액션을 디스패키하기도 한다.

17.2 리덕스 관련 코드

리덕스를 사용할 때는 액션 타입, 액션 생성 함수, 리듀서 코드를 작성해야 한다.

액션 타입은 대문자로 정의하고, 문자열 내용은 ‘모듈 이름/액션 이름’과 같은 형태로 작성한다.

export와 export default의 차이점: export는 여러 개를 내보낼 수 있지만 export default는 단 한 개만 내보낼 수 있다.

immer

객체의 깊이가 깊지 않을수록 추후 불변성을 지켜 가면서 값을 업데이트할 때 수월하다. 하지만 상황에 따라 상태 값들을 하나의 객체 안에 묶어서 넣는 것이 코드의 가독성을 높이는 데 유리하며, 나중에 컴포넌트에 리덕스를 연동할 때도 편하다.

객체의 구조가 복잡해지거나 객체로 이뤄진 배열을 다룰 경우, Immer를 사용하면 훨씬 편리하게 상태를 관리할 수 있다.

connect vs 리덕스 관련 Hook

connect 함수를 사용하여 컨테이너 컴포넡르를 만들었을 경우, 해당 컨테이너 컴포넌트의 부모 컴포넌트가 리렌더링될 때 해당 컨테이너의 props가 바뀌지 않았따면 리렌더링이 자동으로 방지되어 성능이 최적화됨.

반면 userSelect를 사용하여 리덕스 상태를 조회했을 때는 이 최적화 작업이 자동으로 이뤄지지 않으므로, 성능 최적화를 위해서는 React.memo를 컨테이너 컴포넌트에 사용해 줘야함.