본문 바로가기
  • Seizure But Okay Developer
FrontEnd/React 관련자료

리액트 기본 과정 정리 - 9 [불변성을 지키는 이유와 업데이트 최적화]

by Sky_Developer 2019. 12. 13.
이 글은 Velopert 님의 인프런 - 누구든지 하는 리액트 강의 를 참고하여 작성한 글입니다. 혹시 저작권 상의 문제가 발생할시에 내리도록 하겠습니다.

개요

리액트 강의를 듣고 기본 개념을 정리하고 기억하기 위해 작성하였습니다. 공부하시는 다른 분들께 도움이 되었으면 합니다.

 

이번 글에서는 불변성을 왜 유지하는지, 그리고 컴포넌트 업데이트 성능 최적화는 어떻게 이뤄지는지에 대해서 알아보겠습니다.

 

데이터 필터링 구현하기

우선 불변성의 중요성을 알아보는 과정에서 이름으로 전화번호를 찾는 데이터 필터링 기능을 구현해보겠습니다.

 

일단 App 컴포넌트에서 state에 기본값을 등록해놓겠습니다. 컴포넌트 업데이트 최적화 작업을 할 것인데 매번 값을 등록하는 것보다 사전에 값이 준비되어 있는 것이 작업할 때 편하기 때문입니다.

 

이렇게 등록한 상태에서 PhoneInfo 컴포넌트의 render 함수에서 console.log(name) 과 같이 하고 PhoneInfoList 의 render 함수에는 console.log('rendering list') 라고 입력 후 결과를 확인해보겠습니다.

 

실행 결과

결과들이 잘 보입니다. 이 때 새로운 값을 등록하면 어떻게 되는지 봅시다.

 

실행 결과

빨간 테두리 박스를 친 것과 같이 결과가 나옵니다. 컴포넌트들의 렌더링 함수가 한번 더 호출이 되어 나온 결과인 것이죠. 그런데 지금은 값이 하나 추가될 때 다른 것들도 또 다시 렌더링 되는 상황입니다. 실제 바뀌지 않는 컴포넌트들은 DOM 변화가 일어나진 않겠지만 Virtual DOM에는 리렌더링을 합니다.

 

데이터를 새로 등록할 때 기존의 컴포넌트들은 영향을 받을 필요가 없습니다. 그러나 리액트는 기본적으로 App 컴포넌트의 상태가 업데이트 되면 컴포넌트의 리렌더링이 발생하게 되고, 컴포넌트가 리렌더링되면 그 컴포넌트의 자식 컴포넌트 또한 리렌더링이 되도록 설계가 되어있습니다. 단계별로 다음과 같이 이뤄집니다.

  1. App 컴포넌트 상태 업데이트 됨
  2. App 컴포넌트가 리렌더링을 함
  3. PhoneInfoList도 렌더링이 발생함
  4. 내부에 있는 PhoneInfo 컴포넌트들도 한번 더 렌더링을 함

만약 리스트 내부의 아이템이 몇 백개, 몇 천개가 된다면 이렇게 Virtual DOM에 렌더링 하는 자원을 아낄 수 있으면 아끼는 게 좋습니다. 

 

그러므로 낭비되는 자원을 아끼고 업데이트가 불필요할 때는 하지 않도록 작업을 하기 위해 shouldComponentUpdate API를 사용합니다. 성능 최적화를 위해서 PhoneInfoList 컴포넌트에서 shouldComponentUpdate 함수를 작성해줍니다. reactjs code snippets 익스텐션을 설치했다면 scu 라고 입력하면 됩니다.

 

shouldComponentUpdate를 따로 구현하지 않았다면 이 함수는 기본적으로 true를 반환합니다. 이는 부모 컴포넌트가 업데이트 되던 props, state 가 업데이트 되던 언제나 render 함수가 호출되는 거죠.

 

우리는 state 가 바뀔 때 언제나 업데이트를 해줄 것입니다. 그 외에 prosp로 받아온 info 값이 달라졌을 때에도 업데이트를 해주고 만약 state 값도 똑같고 props의 info 값도 똑같으면 render 함수를 호출하지 않습니다.

 

 

콘솔을 확인하면 변화가 필요하지 않을 때 render 함수가 호출되지 않는 것을 확인할 수 있습니다.

 

추가 및 삭제 작업진행 후 결과

 

불변성에 대해 알아보자.

 

추가적으로, App 컴포넌트에서 업데이트를 하게 될 때 불변성을 유지해주지 않으면 어떤 결과가 발생하는지 알아보겠습니다.

 

 

위 사진의 코드처럼 information 배열에 concat을 사용하지 않고 push 로 기존 배열을 직접적으로 바꾸게 되면 아무런 리렌더링이 되지 않습니다. 만약 setState 함수 내에서 information : this.state.information.push({ .... }) 와 같이 작성하여 강제로 변하도록 한다면 리렌더링은 됩니다. 하지만 그렇게 하면 shouldComponentUpdate 함수에서 prosp, state 값의 변화 유무에 따라 업데이트 하거나 하지 않도록 동작이 되지 않습니다.

 

예제를 통해 살펴보겠습니다. 다음 코드를 보세요.

 

arr 을 선언하고 이를 anotherArray 에 할당합니다. 그리고 array 에 새로운 값을 push 해주었을 때 anotherArray의 값은 어떻게 될까요? 똑같이 3이 증가 되었습니다. 그리고 둘을 비교하면 똑같다고 나옵니다. arr 과 anotherArray는 레퍼런스 이름을 다르지만, 자바스크립트 특성상 가리키고 있는 배열은 같으므로 서로 같은 배열입니다.

객체도 이와 똑같습니다. object 객체를 만들고 이를 anotherObject 에 할당한 후, object의 새로운 프로퍼티에 값을 할당합니다. anotherObject의 값 또한 확인해보면 변경이 되어 있고 둘을 서로를 비교하면 똑같다고 나옵니다.

 

이와 같은 문제점은 shouldComponentUpdate에서 로직을 구현할 때 굉장히 머리 아파지게 됩니다. 단순히 객체가 다른지 확인을 하기 위해 내부에 있는 값들을 하나하나 확인해야 하는 상황에 처할 수 있게 됩니다.

 

하지만 불변성을 유지한다면 어떻게 될까요? 다음 코드를 보세요.

 

spread 연산자, concat 메서드 사용

spread 연산자와 concat 메서드를 사용해 기존 배열은 변하지 않은 상태로 그대로 두면서 새로운 배열을 만들어 냅니다. 그 결과 배열들을 비교했을 때 서로 다른 값이라고 나옵니다. 이번에는 객체의 경우를 살펴볼게요.

 

spread 연산자 사용

spread 연산자로 만든 anotherObject는 object 와 다른 값인 것을 알 수 있습니다.

 

또 spread 연산자를 아래와 같이 사용하면 기존 객체의 값을 수정해서 새로운 객체를 만들수도 있습니다.

 

이러한 방법은 React 및 Redux에서 불변성을 유지하면서 상태를 관리할 때 많이 사용이 되므로 꼭 눈여겨둡시다.

 

이처럼 shouldComponentUpdate에서 불변성을 유지해주지 않으면 굉장히 복잡해질 수 있으므로, 리액트에서 객체나 배열을 수정하게 될 때는 언제나 불변성을 유지해줘야 된다는 것을 명심하기 바랍니다.

 

추가적으로 다음 그림과 같이 중첩된 객체, 내부적으로 깊은 객체의 경우 불변성을 유지하면서 내부에 있는 값을 수정해야 할 때 처리하기 난감할 수도 있습니다.

 

중첩된 객체, 깊은 객체

이런 작업을 하게 될 때 spread 연산자를 쓰면 처리가 복잡해질 수도 있습니다. 이럴 때 사용하면 편한 라이브러리들이 있습니다. Immutable.js, Immer.js 가 대표적입니다.

 

기능마저 구현하기

그럼 구현하던 기능을 마저 끝내겠습니다. 다음 코드를 보세요.

 

 

state 값에 keyword를 추가하고, input 폼에 값을 입력한 값을 keyword에 넣어주겠습니다. handleChange 함수 내에서 이벤트 객체를 받아 이전에 했던 것처럼 input 태그의 value 값을 받아서 keyword에 설정합니다.

 

그리고 input 태그에 placeholder, value, onChange 이벤트를 설정하고 이벤트와 handleChange 함수를 연결해줍니다.

이제 keyword에 따라 props로 전달될 배열을 필터링 해줘야 하는데요, 위 코드처럼 filteredList를 변수로 선언해서 작성을 해도 되고, PhoneInfoList 태그 내에서 바로 함수를 다음과 같이 선언해도 됩니다.

 

 

(코드에서 indexOf 함수는 해당 문자열과 매개변수 간에 일치하는 텍스트가 하나라도 있으면 0을 반환하고, 아무것도 일치하지 않으면 -1 을 반환합니다)

information 배열을 filter 함수를 사용하여 요소 중 keyword 와 일치하는 것들만 새롭게 배열을 만들어 PhoneInfoList 컴포넌트에 전달을 해줍니다. 이제 검색기능을 완성하였습니다.

 

DOM에 접근하는 Ref

마지막으로 DOM에 직접적으로 접근하는 리액트의 Ref 라는 기능을 알아보겠습니다. 지금까지 만든 기능을 보면 새로운 데이터를 입력했을 때 input 폼의 focus가 마지막 input 태그에 남아있게 되는데, 이를 맨 처음 input 태그에 남아있도록 할 것입니다.

이를 위해선 input 태그에 직접적으로 접근을 해야 하는데, 이 때 Ref 를 사용합니다. Ref 를 사용하는 방법에는 두 가지 방식이 있습니다.

 

1. 함수를 사용

다음 코드를 보세요.

 

PhoneForm.js

input 값을 null로 선언하고, 첫번 째 input 태그에서 ref 값을 함수로 작성해줍니다.

 

이 함수는 ref 를 파라미터로 받아와서 이 컴포넌트의 멤버 변수로 ref 값을 넣어주는 작업을 합니다. 그래서 PhoneForm 컴포넌트가 한번 렌더링 되고 나면 멤버변수인 input 값이 파라미터로 받아온 ref 가 되는 것이죠.

 

마지막으로 handleSubmit 함수 내에 this.input.focus() 로 작성을 하는데, 이 작업은 handleSubmit 함수가 호출됐을 때 input 태그 DOM에 직접적으로 접근을 하여 focus를 해줍니다.

 

console.log(this.input)의 결과

이제 값을 새로 등록했을 때 첫 번째 input 태그에 focus가 잡히게 됩니다.

2. React.createRef()

이 방법은 React 16.3 에서만 작동합니다.

 

1번 방법과 조금 다른데 코드를 살펴보겠습니다.

 

PhoneForm.js

input에 null 이 아닌 React.createRef() 를 할당해주고, input 태그의 ref 에 함수가 아닌 this.input 를 설정해줍니다.

그리고 조금 전 this.input.focus() 에서 current만 추가하여 this.input.current.focus() 와 같이 작성을 합니다. 이렇게 createRef() 를 사용해서 ref 를 설정해주게 되면 current 라는 값을 통해서 해당 DOM에 대한 접근을 할 수 있게 됩니다.

 

console.log(this.input)의 결과

(콘솔로 this.input.current 를 하면 1번 방법에서 this.input 를 했을 때처럼 input 태그가 결과로 뜹니다)

 

이처럼 Ref 라는 기능은 focus를 주거나, 특정 DOM의 크기를 가져오거나, 특정 DOM의 스크롤의 위치를 설정하거나 스크롤의 크기를 가져오는 등 DOM에 직접적인 접근이 꼭 필요할 때 사용을 합니다.

 

추가적으로 외부 라이브러리를 연동할 때에도 사용을 합니다. 예를 들어 D3, Chartest 같은 차트 라이브러리를 사용하게 될 때 특정 DOM에 그리도록 설정을 한다던지, canvas를 사용한다던지, HTML5의 비디오 관련 라이브러리를 사용하게 될 때도 Ref 를 사용합니다.

 

이제 리액트의 기본 사용법부터 활용법까지 모두 배웠습니다. 앞으로는 어떤 것들을 더 배워야 할지 정리해보겠습니다.

댓글