다시 생각하는 useEffect
- Published on
React를 사용해서 처음 무언가를 만들 때 느꼈던 감정은 어썸함이라기보단 당황스러움이었습니다.
여러 튜토리얼들을 훑으며 이것이 어떻게 작동하는 물건인지 여러 버튼들을 눌러보기 바빴습니다. 그러면서 제 머릿속에는 점차 혼란스러운 생각들이 자리잡기 시작했습니다. JSX는 html 태그가 되는 신기한 물건이고 useEffect는 뭔가 화면에 그려진 다음에 불리는 함수! 라는 이상한 생각들이요.
해당 프레임워크에 대한 불분명한 이해를 통해 태어난 생각들은 곧 큰 버그들과 두통들을 불러왔습니다. 그리고 제게 가장 큰 두통을 준 것 중에 하나는 useEffect였습니다.
useEffect의 공식 문서 설명은 이러합니다.
useEffect(didUpdate);
명령형 또는 어떤 effect를 발생하는 함수를 인자로 받습니다.변형, 구독, 타이머, 로깅 또는 다른 부작용(side effects)은 (React의 렌더링 단계에 따르면) 함수 컴포넌트의 본문 안에서는 허용되지 않습니다. 이를 수행한다면 그것은 매우 혼란스러운 버그 및 UI의 불일치를 야기하게 될 것입니다.
대신에 useEffect를 사용하세요. useEffect에 전달된 함수는 화면에 렌더링이 완료된 후에 수행되게 될 것입니다. React의 순수한 함수적인 세계에서 명령적인 세계로의 탈출구로 생각하세요.
음, 문서에 따르면 사이드 이펙트를 사용하는 행위들을 해당 함수 내부에서 할 수 없으니 해당 Api를 이용해서 사용해야 하는군요. 그런데 문서를 읽고 나서도 몇가지 풀리지 않는 궁금증이 생깁니다.
- 애초에 왜 useEffect를 사용해야 하는걸까요?
- 변형, 구독, 타이머, 로깅과 같은 행위는 왜 함수 컴포넌트 내부에서는 허용되지 않는 걸까요?
- useEffect에 전달된 함수는 화면에 렌더링이 완료된 후에 수행되게 될 것입니다는 대체 무슨 말일까요?
React는 useEffect 말고도 많은 Api들을 제공합니다. 이들을 사용하는 것은 그리 어렵지 않습니다. 충분히 많은 레퍼런스가 있고, 사용법 또한 그리 어렵지 않으니까요. 그러나 올바른 멘탈 모델 없이 useEffect를 휘두르다 보면 얼마 가지 않아 저처럼 수많은 버그들을 마주하게 될 것입니다.
물론 망치를 쓰기 위해 망치가 왜 생겨났는지, 망치를 만든 개발자의 관점은 무엇인지 이해할 필요는 없습니다만, 이러한 부분을 이해하고 쓰는 것과 아닌 것은 분명 큰 차이를 보일 것이라 생각합니다.
그래서 useEffect에 대한 궁금증에 답하기 전에 먼저 2011년으로 돌아가 보도록 하겠습니다.
Back to 2011
Why did we build React?
React isn’t an MVC framework. Reactive updates are dead simple.
당시 Meta(당시에는 facebook)의 기술 스택은 php
와 인터랙션을 위한 조금의 Javascript으로 구성되어 있었습니다. Meta는 급속도로 성장하는 중이었고, 규모가 커지면서 개발자들은 점점 유지 보수에 많은 어려움을 겪고 있었습니다. 그 중 하나는 빈 알림 문제가 있었죠. 사용자의 알림에는 분명 빨간 불이 떴는데, 막상 눌러 보면 아무런 알림이 존재하지 않는 것이었습니다.
당시 Meta가 당면한 과제는 다음과 같았습니다.
알림 버그 등등을 촉발시키는 MVC 패턴을 개선해야 한다. UI 업데이트 사항을 더 쉽게 반영할 수 있어야 한다. 당시에 다른 웹 개발 프레임워크로는 Google의 Angular가 있었으나 타사의 프레임워크를 쓰기는 싫었던 것인지, Meta는 자신의 프레임워크를 개발하기로 결정합니다. 그렇게 태어난 것이 바로 React입니다.
다들 알고 있겠지만 React는 기존의 Javascript 어플리케이션들이 브라우저의 DOM을 up-to-date 형식으로 직접 업데이트 시키는 것과 다른 관점을 선택하였습니다. 계속해서 render 메소드를 호출함으로써 업데이트 사항들을 한번에 반영하는 것이었죠.
React가 채택한 새로운 방식은 이렇습니다. render 메소드가 호출됩니다. React는 react element들로 이루어진 트리를 브라우저에 건네 줍니다. 만약 데이터가 변하게 되면, 다시금 render 메소드가 호출됩니다. 다시 트리를 생성하고 이전 트리와 비교합니다. 비교된 값을 브라우저에게 건네 줍니다. 만약 다시 변한다면, 다시 render 메소드가 호출됩니다. (해당 과정의 효율성을 위해 virtual dom과 같은 기술들이 도입되었지만 해당 글에서 설명하지는 않겠습니다. )
기존의 방식이 빈 알림 문제와 같이 유지보수에 있어 많은 코스트를 일으키고 있었으므로, 새로운 방식을 택하는 것은 어쩌면 기정사실이었을 것입니다.
순수성과 멱등성
React가 웹 어플리케이션을 만드는 새운 방식은 계속되는 함수의 호출을 통한 계속되는 트리의 생성, 생성된 트리의 계속되는 비교입니다. React적인 사고방식 하에서 모든 것은 새로 생성되고 비교되고, 없어지고를 반복합니다.
Redux의 개발자인 Dan Abramov 는 자신의 글에서 이렇게 말하고 있습니다.
…React는 결과에 일정한 ‘패턴’이 없을 때 적합하지 않습니다. 예를 들어 React는 Twitter 클라이언트를 작성하는 데 도움이 되지만 3D 파이프 스크린 세이버에는 별로 유용하지 않습니다.
계속되는 트리를 비교하기 위해서는 트리에 공통점이 존재해야 합니다. 이전 트리와 새로 렌더링된 트리가 100% 다르다면, 트리를 비교하는 의미가 없을 것입니다. 따라서 당연하게도 React는 외부 변수에 의존하지 않고, 같은 인풋값에 같은 결과값을 내는 것을 원칙으로 합니다. 추상적으로 설명해보겠습니다.
만약에 해당 트리가 외부 변수에 의존한다고 생각해 봅시다. 이전 트리와 다음 트리가 바뀌었는데, 그 이유는 React 외부의 어떤 요소 때문입니다. 렌더링 도중에 어떤 문제-아까 말씀드린 빈 알람 문제가 생겼다고 생각해 봅시다. 효과적으로 디버깅 하기 어려울 것입니다. 2011년으로 돌아가게 되겠죠.
같은 인풋값에 같은 결과값을 내야 하는 이유 또한 와 비슷합니다. 예측 가능한 패턴화가 React를 위한 필수적인 요소이기 때문입니다.
그러나 우리가 React를 사용해서 만드려고 하는 것은 단순히 a일때 b를 보여주는 어플리케이션이 아닙니다. 인스타그램은 끊임없이 다른 사람들의 피드를 서버에서 받아 오고, 유튜브에는 끊임없이 좋아요가 눌리는 것처럼, 많은 모던 어플리케이션들은 외부와 끊임없이 데이터를 주고 받습니다.
그러면 React는 결과에 패턴이 있을때 적합하다는데, 외부에서 데이터가 온다면 어떻게 하죠? 외부 데이터가 결과를 바꾼다면, 패턴이 망가지는 것이 아닐까요?
useEffect
사이드 이펙트는 모던 어플리케이션 개발에 있어서 피할 수 없는 부분입니다. 할 수 없으면 즐기라는 말처럼, 답은 간단합니다. 외부에서 오는 데이터 또한 React 의 끊임없는 비교 과정 속에 포함시키면 됩니다.
정확히 말하면 계속되는 함수의 호출을 통한 계속되는 트리의 생성, 생성된 트리의 계속되는 비교 과정에 외부 데이터를 포함시키는 과정을 추가하면 됩니다. BOOM. 그리고 이것이 바로 useEffect가 하는 일입니다. React가 side Effect를 use할 수 있게 도와주는 Api 인 것입니다.
Dan Abramov가 설명한 useEffect 가이드에서 그는 useEffect를 React 트리 바깥의 외부의 데이터를 동기화 시킬 수 있게 해주는 것이라고 설명하고 있습니다. 따라서 정확히 말하면 외부 데이터를 포함시키는 것이 아니라 동기화에 가깝습니다.
- React 트리가 생성됩니다
- React 트리를 전 트리와 비교합니다
- 새로운 트리를 생성합니다.
- 실제로 업데이트를 수행합니다.
- 외부 데이터들을 useEffect가 다음 트리에 사용할 수 있도록 동기화합니다.
이렇게 동기화를 통해, 패턴화를 방해하지 않으면서 성공적으로 React 외부의 데이터를 사용할 수 있게 되었습니다.
답하기
- 애초에 왜 useEffect를 사용해야 하는걸까요?
- 변형, 구독, 타이머, 로깅과 같은 행위는 왜 함수 컴포넌트 내부에서는 허용되지 않는 걸까요?
- useEffect에 전달된 함수는 화면에 렌더링이 완료된 후에 수행되게 될 것입니다는 대체 무슨 말일까요?
아까 공식 문서를 읽으면서 든 몇가지 궁금증들은 이미 이 글을 읽으면서 이미 해소되셨을 것이라 생각합니다. useEffect를 사용하는 이유는 React의 멘탈 모델-패턴을 망가트리지 않는 범위 내에서 외부의 정보를 사용하기 위함입니다. 비슷한 이유로 변형, 구독, 타이머, 로깅과 같은 행위는 왜 함수 컴포넌트 내부에서는 허용되지 않습니다. 만약에 내부 컴포넌트에서 useEffect를 사용하지 않고 외부 데이터를 통해 무언가를 변형할 경우, 우리는 다시금 빈 알림 문제를 마주하게 될 것입니다.
useEffect에 전달된 함수는 사이드 이펙트를 처리하기 위해 사용됩니다. 만약에 화면이 렌더링 되기 전에 사이드 이펙트가 처리된다면, 우리는 패턴을 보장할 수 없을 것입니다.
React의 모든 것은 끊임없이 변화합니다. 10년이 지나고, 100년이 지나도 렌더링되고 리렌더링을 반복할 것입니다. 그러나 아무리 많은 시간이 지나더라도, 그 누구라도 변화 과정을 방해할 수 없습니다. 외부의 정보가 필요하다면, 변화를 막는 것이 아니라 해당 정보를 변화의 흐름의 일부분으로 만들어야 합니다.
마치면서
정리해보도록 하겠습니다. 2011년 Meta는 자신들이 맞이한 문제들을 해결하기 위해 새로운 프레임워크인 React를 개발했습니다. React는 문제 해결을 위해 기존의 자바스크립트 어플리케이션과 달리 계속해서 tree를 생성하고 비교하고 업데이트 하는 방법을 택했습니다.
그러나 해당 방식을 사용함에 있어 외부 데이터를 임의로 처리한다면 많은 문제를 맞이 할 것이 분명하므로, 외부 데이터를 React-way로 핸들링 할 수 있도록 방법을 제공해주는 것이 바로 useEffect입니다.
*해당 글에서는 글이 너무 길어지는 것을 막기 위해 클래스형 컴포넌트에 대한 설명은 의도적으로 넣지 않았습니다.\