(번역) React, 인라인 함수, 그리고 성능

Hwidong Bae
Steady Study
Published in
14 min readApr 10, 2018

--

원문: React, Inline Functions, and Performance by @ryanflorence (2017.10.07)
이 글에 대한 모든 권리는 원 저작자에게 있습니다. All rights are reserved to the original author.

우리 부부는 얼마 전 대대적으로 집을 리모델링했다. 우리는 아주 흥분해서 사람들에게 집 구석구석을 보여주고 다녔다. 장모님에게도 보여드렸다. 그녀는 아름답게 리모델링된 침실에 들어와서는, 환상적인 프레임으로 꾸며진 창문을 보고 이렇게 말했다: “블라인드는 없니?” 😐

우리 부부의 새 침실이다. 이거 완전 잡지 사진 아닌가? 물론 블라인드는 없다.

나는 React에 대해서 이야기할 때면 그 때와 같은 감정을 느끼게 된다. 워크숍의 첫 번째 강의를 진행하면서, 몇몇 멋진 새 오픈소스 소프트웨어를 보여주고, 예외 없이 누군가가 말한다: “인라인 함수요? 그거 느리다고 들었는데요.”

항상 이런 식인 건 아니었다. 하지만 적어도 지난 수개월간은 매일 들은 것 같다. 강사이자 라이브러리 개발자로서 이는 참 지치는 일이었다. 불행히도, 나는 그냥 바보였고, 사람들에게 도움이 되는 글을 쓰기보다는 트위터에서 설교하는 것을 택했었다. 즉, 이 글은 더 나은 옵션을 위한 시도다. 😂

“인라인 함수”는 무엇인가

React에서 인라인 함수는 React가 “렌더링"될 때 정의되는 함수를 말한다. React에서의 “렌더링"에는 사람들이 종종 헷갈려하는 두 가지 뜻이 있다: (1) 업데이트가 일어났을 때, 컴포넌트의 render 함수를 호출함으로서 React 엘리먼트를 컴포넌트에서 받아오는 것과 (2) DOM을 업데이트하는 실제 렌더링. 이 글에서는 “렌더링"을 (1)의 의미로 사용하겠다.

다음은 인라인 함수의 몇 가지 예시다:

섣부른 최적화는 만악의 근원이다(Premature optimization is the root of all evil)

글을 더 진행하기 전에, 프로그램을 최적화하는 방법에 대해 이야기할 필요가 있다. 지나가는 성능 전문가를 아무나 붙잡고 물어보면, 백이면 백 섣부르게 프로그램을 최적화하지 말라고 할 것이다. 그렇다. 100명 중 100명이 그렇게 말할 것이다. 성능에 대해 오랜 경험이 있는 모든 사람들은 섣부르게 코드를 최적화하지 말라고 한다.

측정하지 않는다면, 최적화한 결과가 더 나은지는커녕 그 최적화로 상황이 더 악화되었는지도 알 수 없다!

나는 내 친구 Ralph Holzmann의 gzip이 어떻게 작동하는지에 대한 강연을 듣고 더욱 이 생각에 확신을 가지게 됐다. 그는 LABjs라는 오래된 스크립트 로딩 라이브러리로 행한 실험에 대해 발표했다. 이 영상의 30:02 ~ 32:35에서 직접 듣거나, 아니면 그냥 계속 이 글을 읽기를.

당시 LABjs의 소스코드는 성능상에 약간 문제가 있어보이는 행동을 하고 있었다. 평범한 오브젝트 표현방식( obj.foo )대신, 오브젝트의 키를 스트링으로 저장하고 배열로 접근하고 있었다( obj[stringForFoo] ). 이는 코드 최소화minifying와 압축gzipping을 하고 나면, 부자연스럽게 쓰인 코드가 자연스럽게 쓰인 것보다 더 용량이 작을 것이라는 아이디어에서 비롯되었다. 코드를 확인해 보라.

Ralph는 코드를 포크해와서, 최적화한 부분을 제거하고, 최소화와 압축에서 어떻게 최적화할지 생각하지 않은 채 자연스러운 방식으로 코드를 재작성했다.

결과는, 그 “최적화"를 제거했더니 파일 용량이 5.3% 더 줄어들었다! 측정하지 않는다면, 최적화 결과가 더 나은지는커녕 그 최적화로 상황이 더 악화었는지도 알 수 없다!

섣부른 최적화는 개발 시간을 폭증시키고 코드를 더럽게 만들 뿐 아니라, LABjs의 예시처럼 오히려 성능 문제의 원인이 될 수도 있다. 성능 문제에 대해 그저 상상만 하는 대신 측정을 했다면, 개발 시간은 줄어들었을 것이고, 더 깨끗하면서도 더 좋은 성능의 코드가 만들어졌을 것이다.

섣부르게 최적화하지 마라. 좋다. 이제 React로 돌아가자.

왜 사람들은 인라인 함수가 느리다고 말하는가?

두 가지 이유가 있다. 메모리/가비지 콜렉션에 대한 고려, 그리고 shouldComponentUpdate .

메모리와 가비지 콜렉션

우선, 사람들과 (eslint 설정은) 인라인 함수를 생성하는데 드는 메모리와 가비지 콜렉션 비용에 대해 걱정한다. 화살표 함수arrow function가 널리 퍼지기 이전부터 이 말은 여기저기서 들려왔다. 수많은 코드가 인라인으로 bind 를 했고, 이는 과거에는 안좋은 성능을 보였다. 예를 들면:

Function.prototype.bind 의 성능 문제는 고쳐졌고, 화살표 함수는 네이티브이거나 babel을 통해 순수함수로 트랜스파일된다. 이 두 경우 모두 느리지 않을 것이라고 가정할 수 있다.

그저 앉아있는 채로 “이 코드는 느릴 게 분명해"라고 생각하면 안된다는 걸 기억하라. 자연스러운 방식으로 코딩하고, 그 다음 측정하라. 만약 성능 문제가 있었다면, 고쳐라. 우리는 화살표 함수가 빠름을 증명할 필요가 없다. 누군가는 화살표 함수가 느리다는 걸 증명하려 하겠지만, 그게 증명되기 전까지는 화살표 함수를 안쓰는 건 섣부른 최적화다.

인라인 함수를 생성하는 비용이 ‘인라인 함수 사용을 피하라'는 eslint 규칙을 보증할 만큼 크다면, 컴포넌트를 초기화할 때로 위치를 옮기는 건 어떤가?

섣부르게 최적화함으로써 우리는 컴포넌트의 초기화를 3배 느리게 했다! 모든 핸들러가 인라인이었으면, 최초의 렌더링에서 함수 하나가 만들어졌을 것이다. 대신 우리는 함수 세 개를 만들었다. 사실 아직 아무것도 측정하지 않았기 때문에 우리는 이게 실제로 문제가 된다고 믿을 만한 어떤 근거도 없다.

내 말을 완벽하게 오해하고 싶다면, 가서 새로운 eslint 규칙을 하나 추가하라. “초기 렌더링을 빠르게 하기 위해 모든 함수를 인라인으로 만들어야 한다.”

(역자 주: 저자의 포인트는 ‘인라인 함수를 쓰면 컴포넌트를 첫번째 렌더링하는 데 걸리는 시간이 줄어든다'가 아니라, ‘인라인 함수가 실제로 성능이 좋지 않다는 확실한 증거가 나올때까지는 괜히 부자연스러운 코딩을 할 필요 없다'에 있다.)

PureComponent와 sholudComponentUpdate

이 부분이 실제로 문제가 생길 수 있는 부분이다. 다음 두 가지를 이해함으로써 실제 성능 향상을 볼 수 있다: shouldComponentUpdate 와 자바스크립트의 엄격한 동치 비교strict equality comparisons. 이것들을 잘 이해하지 못한다면, 의도치 않게 당신의 React 코드에 성능 문제가 생길 수 있다.

setState 가 호출되면, React는 오래된 React 요소들과 새 요소들을 비교하고 (이 과정을 reconciliation이라고 한다. 여기에서 읽어볼 수 있다) 그 정보를 토대로 실제 DOM 을 갱신한다. reconciliation은 비교할 요소가 많으면 (거대한 SVG 등) 느려질 수 있다. 이런 경우를 피해갈 수 있도록 React가 제공하는 창구가 shouldComponentUpdate 다.

컴포넌트에 shouldComponentUpdate 가 정의되어 있으면, React가 요소를 비교하기 전에 shouldComponentUpdate 를 체크한다. 이 함수가 false를 리턴하면 React는 요소 비교를 완전히 건너뛴다. 컴포넌트가 충분히 크다면, 이는 성능에 상당한 영향을 미칠 수 있다.

컴포넌트를 최적화하는 가장 일반적인 방법은 React.Component 대신 React.PureComponent 로부터 확장하는 것이다. PureComponent 는 자동으로 shouldComponentUpdate 에서 컴포넌트의 prop들과 state를 비교하도록 되어 있다.

class Avatar extends React.PureComponent { ... }

Avatar 는 이제 업데이트가 필요할 때 prop들과 state에 대해 “엄격한 동치 비교"를 수행할 것이고, 아마 성능이 좋아질 것이다.

엄격한 동치 비교

자바스크립트에는 기본 타입 6개가 있다: 스트링, 숫자, 불리언, null, undefined, 그리고 심볼. 같은 값을 가진 두 기본 타입을 엄격하게 동치 비교하면 true 를 얻을 것이다. 예를 들면:

const one = 1
const uno = 1
one === uno // true

PureComponent 가 prop을 비교할 때에도 엄격한 동치 비교를 사용한다. 이는 인라인에서 기본 타입의 값을 비교할 때 완벽하게 작동한다: <Toggler isOpen={true}/> .

prop 비교는 기본이 아닌 타입을 비교할 때 문제가 생긴다. 자바스크립트에는 기본이 아닌 타입이 Object 하나뿐이다. 함수와 배열과 다른 것들은? 흠, 사실 그것들도 그냥 오브젝트다.

함수는 호출될 수 있다는 특징을 추가로 가진, 일반적인 오브젝트다.
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures

하하, 이래야 자바스크립트답지. 아무튼, 오브젝트에 대한 엄격한 동치 비교는, 겉보기에 같은 값을 가졌더라도 false 를 리턴한다:

따라서 당신이 JSX 코드에 오브젝트를 인라인으로 넣는다면, PureComponent 의 prop 비교(역자 주: shouldComponentUpdate안에 있는)를 통과하여 더 무거운 React 요소 비교로 들어가게 될 것이다. 실제로 React 요소는 변한 게 없기 때문에 사실상 양쪽의 비교에서 모두 시간을 낭비하는 셈이다.

함수는 오브젝트이고, PureComponent 는 prop에 대해 엄격한 동치 비교를 하기 때문에 인라인 함수는 언제나 prop 비교를 통과하여 reconciler의 요소 비교로 들어간다.

예상했다시피 이건 단순히 인라인 함수만의 문제는 아니다. 함수는 그저 오브젝트, 함수, 배열 3개 중 가장 돋보이는 녀석일 뿐이다.

shouldComponentUpdate 가 행복해지려면, 함수의 참조 동일성(referential identity)이 유지되어야 한다. 이는 숙련된 자바스크립트 개발자에겐 어렵지 않은 일이다. 하지만 Michael과 나는 3,500명이 넘는 다양한 경험을 지닌 사람들과 워크숍을 진행하면서, 많은 사람들에게 이게 쉽지 않다는 걸 알고 있었다. ES의 클래스는 아무 도움이 되지 않았기 때문에, 우리는 자바스크립트에서 통하는 방법을 찾기 위해 계속 뒤져봐야 했다.

함수의 참조 동일성을 어떻게 유지할 것인지에 대한 문제에는 놀라울 정도로 긴 토론이 필요했다.

대개는 우리가 열심히 사람들에게 이런 식으로 코딩을 하라고 강제하는 것보다는, eslint config가 사람들에게 경고하게 두는 게 더 낫다. 이제 인라인 함수와 성능 최적화를 어떻게 동시에 이룰 수 있는지를 보여주고자 한다. 그런데 그 전에, 성능과 관련된 내 개인적인 이야기를 들려주겠다.

PureComponent와 관련된 나의 경험

내가 처음 PureRenderMixin(요즘 버전의 React에서는 PureComponent로 바뀐)에 대해 배웠을 때, 여러모로 앱의 성능을 측정해보았다. 그 다음 PureRenderMixin 을 모든 컴포넌트에 추가했다. 다시 앱 성능을 측정했을 때, 나는 사람들에게 PureRenderMixin 으로 내 앱이 얼마나 빨라졌는지를 멋지게 보여줄 수 있으리라 기대했다.

아주 놀랍게도, 내 앱은 더 느려졌다 🤔.

왜 그랬을까? 생각해보자. Component 하나에 대한 비교 연산은 몇 개인가? PureComponent 라면 어떤가? 각각의 답은 “하나"와 “적어도 하나, 때때로 둘"이다. 만약 컴포넌트가 업데이트되는 대부분의 경우에 변경된다면, PureComponent 는 한 번이 아닌 두 번의 비교를 하게 된다( shouldComponentUpdate 에서 prop들과 state를 비교하고, 그 다음 일반적인 요소 비교를 진행). 즉 이 경우에는 PureComponent 를 사용하면 대부분 느려지고, 때때로 빨라진다. 당연히 내 앱에 있는 컴포넌트 중 대다수는 계속 변경되는 녀석들이었고, 전체적으로 앱은 느려졌다. 이런.

성능 문제에 있어서 만능 해결책은 없다(There are no silver bullets). 무조건 측정해봐야 한다.

3가지 시나리오

이 글의 도입부에서 인라인 함수의 3가지 형태를 보였다. 이제 우리가 어느정도 배경지식을 갖췄으니 각 형태에 대해 하나씩 이야기해보겠다. 단, PureComponent 는 그걸 사용하는 게 더 좋다는 측정결과가 나올 때까지 잠깐 치워두도록 하자.

DOM 컴포넌트 이벤트 핸들러

<button
onClick={() => this.setState(…)}
>click</button>

버튼, 인풋, 또는 다른 DOM 컴포넌트에 setState 만 호출하는 이벤트 핸들러를 두는 건 흔한 일이다. 이런 경우 인라인 함수를 쓰는 방법이 가장 깔끔하다. 파일에서 이벤트 핸들러를 찾아 해매는 대신 동일 위치에 두기(Colocation). React 커뮤니티는 대개 동일 위치에 두기를 환영한다.

button 컴포넌트는 (그리고 다른 DOM 컴포넌트들도) 애초에 PureComponent 가 될 수 없으므로, shouldComponentUpdate 의 참조 동일성 문제도 없다.

따라서 이 케이스가 느리다고 생각할 만한 유일한 근거는 그저 함수를 정의하는 데 큰 비용이 들지도 모른다는 것뿐이다. 앞서 논의했듯, 함수 정의에 큰 비용이 든다는 증거는 아무데도 없다. 그건 탁상공론에 불과하다. 증거가 갖춰지기 전까지는 함수 정의에는 아무 문제가 없다.

“사용자 정의 이벤트" 또는 “액션”

<Sidebar onToggle={(isOpen) => {
this.setState({ sidebarIsOpen: isOpen })
}}/>

SidebarPureComponent 라면 prop 비교가 또 문제가 될 것이다. 이 경우 역시, 핸들러가 간단하기 때문에 동일 위치에 두는 게 선호되는 상황이다.

그런데 SidebaronToggle 같은 이벤트를 왜 비교하고 있는가? shouldComponentUpdate 의 비교 연산에 prop을 집어넣을 만한 이유는 두 가지밖에 없다:

  1. 렌더링할 때 그 prop을 사용한다.
  2. componentWillReceiveProps , componentDidUpdate , 또는 componentWillUpdate 에서 그 prop을 이용해서 어떤 액션을 수행한다.

대부분의 on<무언가> prop들은 이 두 가지 조건을 충족하지 않는다. 즉 대부분의 PureComponent 는 필요없는 비교 연산을 하게 되며, 개발자가 쓸데없이 참조 동일성을 유지하게 만든다.

정말 필요한 prop만 비교해야 한다. 그러면 여전히 핸들러를 동일 위치에 둘 수 있고, 성능 향상 또한 꾀할 수 있다(비교 연산 자체도 줄어든다!).

대부분의 컴포넌트에서, 나는 PureComponent 대신 PureComponentMinusHandlers 클래스를 만들어서 상속시키는 걸 권장한다. 단순히 함수에 대한 비교는 무조건 건너뛰게 할 수도 있다. 당신의 상황에 맞게 만들면 된다.

음, 웬만하면 그렇다.

만약 함수를 받아서 그걸 다른 컴포넌트로 바로 넘긴다면 최신 상태가 유지되지 않을 수 있다(stale). 다음을 보라:

codesandbox에서 이 앱이 어떻게 돌아가는지 확인할 수 있다.

따라서 PureComponentMinusHandlers 에서 상속받는 아이디어를 따르고자 한다면, 해당 핸들러를 다른 컴포넌트에 바로 넘기지 않도록 주의하라. 어떤 방식으로든 그걸 감싸서 전달해야 한다.

이제 우리는 참조 동일성을 유지하거나, 아니면 참조 동일성을 피해야 한다! 성능 최적화의 세계에 온 것을 환영한다. 최소한 이 접근법은 최적화된 컴포넌트 내부만 신경쓰면, 그 컴포넌트를 사용하는 코드는 생각하지 않아도 된다.

솔직히 말하면 이 예시 앱은 내가 이 글을 공개한 뒤 Andrew Clark이 제기한 문제 때문에 나중에 추가한 것이다. 당신은 내가 참조 동일성을 언제 유지해야 할지 잘 알고 있다고 생각했겠지만… 😂

render prop

Render prop은 공유된 state를 관리하고 조합하는 컴포넌트를 만드는 패턴이다. (여기에서 더 자세한 글을 읽어볼 수 있다.) render prop의 내용은 해당 컴포넌트가 알 수 없다. 예를 들면:

그러니까, 인라인 render prop 함수는 shouldComponentUpdate 는 커녕 PureComponent 를 써야할지 말지조차 알 수 없기 때문에, 이 부분에서 최적화의 대상이 아니다.

즉 이번에도, 유일하게 남은 문제는 함수를 정의하는 게 느릴 것이라는 믿음이다. 첫 번째 예시에서의 이야기를 반복하자면, 그런 증거는 없다. 탁상공론이다.

요약

  1. 자연스러운 코드를 작성하라.
  2. 느린 경로를 찾기 위해 측정하라. 어떻게? 이렇게.
  3. PureComponentshouldComponentUpdate 를 정말 필요할 때에만, prop 함수를 제외하고 사용하라 (prop으로 넘긴 함수가 액션으로 사용될 게 아니라면).

당신이 섣부른 최적화가 좋지 않다고 진심으로 믿는 사람이라면, 인라인 함수가 빠르다는 증거는 필요 없다. 느리다는 증거가 필요하다.

--

--