컴포넌트의 순수성 유지
- Published on
- 리액트 컴포넌트의 순수성을 유지하는 중요성과 순수 함수의 개념에 대한 내용을 다루고 있습니다.
Table of Contents
일부 자바스크립트 함수는 순수합니다. 순수 함수는 계산만 수행하고 그 이상의 작업을 수행하지 않습니다. 컴포넌트를 엄격하게 순수 함수로 작성함으로써 코드베이스가 커져도 혼란스러운 버그와 예측할 수 없는 동작을 피할 수 있습니다. 그러나 이러한 이점을 얻으려면 몇 가지 규칙을 준수해야 합니다.
순수성: 컴포넌트를 수식으로 취급하기
컴퓨터 과학(특히 함수형 프로그래밍의 세계)에서 순수 함수는 다음 특징을 갖는 함수입니다:
- 자신의 일에만 전념합니다. 호출되기 전에 존재한 객체나 변수를 변경하지 않습니다.
- 같은 입력에 대해 항상 같은 출력을 반환합니다.
이미 순수 함수의 예시에 익숙할 수도 있습니다: 수학의 공식입니다.
다음 수학 공식을 고려해보세요: y = 2x
.
- 만약
x = 2
이면y = 4
입니다. 항상입니다. - 만약
x = 3
이면y = 6
입니다. 항상입니다. x = 3
이라면y
는 시간이나 주식 시장의 상태에 따라 때로는 9, -1 또는 2.5와 같이 달라질 수 없습니다.- 만약
y = 2x
이고x = 3
이라면 y는 항상 6이 됩니다.
이를 자바스크립트 함수로 만든다면 다음과 같을 것입니다:
function double(number) {
return 2 * number;
}
위의 예시에서 double은 순수 함수입니다. 3을 전달하면 항상 6을 반환합니다.
리액트는 이 개념을 기반으로 설계되었습니다. 리액트는 작성하는 모든 컴포넌트가 순수 함수라고 가정합니다. 즉, 리액트 컴포넌트는 항상 동일한 JSX를 동일한 입력에 대해 반환해야 합니다:
Recipe에 drinkers={2}
를 전달하면 항상 2잔의 물을 포함하는 JSX를 반환합니다.
drinkers={4}
를 전달하면 항상 4잔의 물을 포함하는 JSX를 반환합니다.
마치 수학 공식과 같이요.
컴포넌트를 레시피처럼 생각할 수 있습니다: 레시피를 따르고 요리 과정 중에 새로운 재료를 도입하지 않는다면 매번 동일한 요리를 얻을 수 있습니다. 그 "요리"는 컴포넌트가 리액트에게 제공하는 JSX입니다.
Side Effects: (무)의도적인 결과
리액트의 렌더링 과정은 항상 순수해야 합니다. 컴포넌트는 JSX만 반환해야 하며 렌더링 이전에 존재한 객체나 변수를 변경해서는 안 됩니다. 그렇게 되면 순수하지 않아집니다!
다음은 이 규칙을 어긴 컴포넌트의 예시입니다:
이 컴포넌트는 외부에서 선언된 guest 변수를 읽고 쓰고 있습니다. 이는 이 컴포넌트를 여러 번 호출하면 다른 JSX가 생성됨을 의미합니다! 게다가 guest를 읽는 다른 컴포넌트도 렌더링될 때마다 다른 JSX를 생성하게 됩니다! 이는 예측할 수 없는 동작입니다.
다시 수식 y = 2x로 돌아가보면, 이제 x = 2라도 y = 4인지 믿을 수 없습니다. 테스트가 실패할 수 있고 사용자가 혼란스러워질 수 있으며 비행기가 하늘에서 떨어질 수 있다는 것을 상상해보세요. 혼란스러운 버그가 발생할 수 있다는 점을 알 수 있습니다!
guest를 프롭으로 전달하는 방식으로 이 컴포넌트를 수정할 수 있습니다:
이제 컴포넌트는 순수하며 반환하는 JSX는 guest 프롭에만 의존합니다.
일반적으로 컴포넌트가 렌더링되는 순서에 의존하지 않아야 합니다. y = 2x를 y = 5x보다 먼저 또는 나중에 호출하는 것은 중요하지 않습니다. 두 수식은 독립적으로 해결됩니다. 마찬가지로 각 컴포넌트는 "자기 일"만 처리하고 렌더링 중에 다른 컴포넌트와 조정하거나 의존해서는 안 됩니다. 렌더링은 학교 시험과 비슷합니다: 각
컴포넌트는 스스로 JSX를 계산해야 합니다!
자세히 알아보기: Strict 모드를 사용하여 순수하지 않은 계산 찾기
아직 모두 사용해보지는 않았을 수 있지만, 리액트에서는 렌더링 중에 읽을 수 있는 세 가지 종류의 입력이 있습니다: Props, 상태 및 컨텍스트입니다. 이러한 입력은 항상 읽기 전용으로 취급해야 합니다.
사용자 입력에 응답하여 무언가를 변경하려면 변수에 쓰는 대신 상태를 설정해야 합니다. 컴포넌트가 렌더링되는 동안 기존 변수나 객체를 변경해서는 안 됩니다.
리액트는 "Strict 모드"를 제공하여 개발 중에 각 컴포넌트 함수를 두 번 호출합니다. Strict 모드는 컴포넌트 함수를 두 번 호출하여 이러한 규칙을 어긴 컴포넌트를 찾는 데 도움을 줍니다.
원래 예시에서 "Guest #2", "Guest #4", "Guest #6"이 아닌 "Guest #1", "Guest #2", "Guest #3"이 표시되는 것을 확인하세요. 원래 함수는 순수하지 않았기 때문에 두 번 호출하면 오작동합니다. 그러나 수정된 순수 버전은 함수를 매번 두 번 호출하더라도 작동합니다. 순수 함수는 계산만 수행하므로 두 번 호출해도 아무것도 변경되지 않습니다
- 마치 double(2)를 두 번 호출해도 반환 값이 변경되지 않고, y = 2x를 두 번 푸는 것이 y를 변경하지 않는 것과 같습니다. 같은 입력, 같은 출력. 항상 그렇습니다.
Strict 모드는 프로덕션에서는 효과가 없으므로 사용자에게 앱이 느려지지 않습니다. Strict 모드를 사용하려면 루트 컴포넌트를 <React.StrictMode>
로 감싸면 됩니다. 일부 프레임워크는 기본적으로 이를 수행합니다.
로컬 변이: 컴포넌트의 작은 비밀
위의 예시에서 문제는 컴포넌트가 렌더링 중에 기존 변수를 변경했다는 것입니다. 이는 약간 무섭게 들리도록 "변이(mutations)"라고 합니다. 순수 함수는 함수의 범위 외부의 변수나 이전에 생성된 객체를 변경하지 않습니다- 이로 인해 순수하지 않습니다!
그러나 렌더링하는 동안에만 생성한 변수와 객체를 변경하는 것은 전혀 문제가 없습니다. 이 예시에서는 []
배열을 생성하고 cups 변수에 할당한 다음 여러 잔의 컵을 push하는 것입니다:
cups 변수나 []
배열이 TeaGathering 함수 외부에서 생성되었다면 큰 문제가 될 것입니다! 이렇게 하면 기존 객체를 변경하고 배열에 항목을 push하는 것입니다.
하지만 이는 TeaGathering 내에서 렌더링 동안 동일한 시기에 생성되었기 때문에 괜찮습니다. TeaGathering 외부의 코드는 이 일이 발생했다는 사실을 절대로 알 수 없습니다. 이를 **"로컬 변이"**라고 합니다- 컴포넌트의 작은 비밀입니다.
Side Effects을 일으킬 수 있는 위치
함수형 프로그래밍은 순수성에 크게 의존하지만, 어느 시점에선가 무언가가 변경되어야 합니다. 그것이 프로그래밍의 목적이기 때문입니다! 이러한 변경- 화면 업데이트, 애니메이션 시작, 데이터 변경-을 **부작용(side effect)**라고 합니다. 이들은 렌더링 중에 발생하지 않는 것들입니다.
리액트에서 부작용은 일반적으로 이벤트 핸들러 내에 속합니다. 이벤트 핸들러는 특정 동작(예: 버튼 클릭)을 수행할 때 리액트가 실행하는 함수입니다. 이벤트 핸들러는 컴포넌트 내에서 정의되었지만 렌더링 중에 실행되지 않습니다! 따라서 이벤트 핸들러는 순수할 필요가 없습니다.
모든 다른 옵션을 모두 고갈하고도 올바른 부작용용 이벤트 핸들러를 찾을 수 없다면 여전히 반환된 JSX에 useEffect 호출을 사용하여 부차적으로 연결할 수 있습니다. 이렇게 하면 리액트가 렌더링 후, 부작용이 허용되는 시점에 실행하도록 지시할 수 있습니다. 그러나 이 접근 방식은 마지막 수단으로 사용해야 합니다.
가능한 경우 논리를 렌더링 자체로 표현하려고 노력해보세요. 이렇게 하면 얼마나 멀리 갈 수 있는지 놀라실 겁니다!
자세히 알아보기: 리액트가 순수성을 중요하게 여기는 이유
순수 함수를 작성하는 것은 일부 습관과 훈련이 필요하지만, 놀라운 기회를 열어줍니다:
- 컴포넌트는 다른 환경(예: 서버)에서 실행될 수 있습니다! 입력에 대해 동일한 결과를 반환하기 때문에 하나의 컴포넌트가 여러 사용자 요청에 대해 서비스를 제공할 수 있습니다.
- 순수 함수는 항상 동일한 결과를 반환하므로 입력이 변경되지 않은 컴포넌트의 렌더링을 건너뛰어 성능을 향상시킬 수 있습니다. 이는 순수 함수를 캐시해도 안전하다는 것을 의미합니다.
- 깊은 컴포넌트 트리를 렌더링하는 도중에 데이터가 변경되면 리액트는 구식 렌더링을 완료하기 전에 다시 렌더링을 재시작할 수 있습니다. 순수성으로 인해 언제든지 계산을 중단해도 안전합니다.
리액트가 구축하고 있는 모든 새로운 기능은 순수성을 활용합니다. 데이터 가져오기부터 애니메이션, 성능까지, 컴포넌트를 순수하게 유지하면 리액트 패러다임의 힘을 발휘할 수 있습니다.
요약
- 컴포넌트는 순수해야 합니다. 즉:
- 자신의 일에만 전념합니다. 렌더링하기 전에 존재한 객체나 변수를 변경해서는 안 됩니다.
- 같은 입력에 대해 항상 같은 출력을 반환합니다. 컴포넌트는 항상 동일한 JSX를 반환해야 합니다.
- 렌더링은 언제든지 발생할 수 있으므로 컴포넌트는 다른 컴포넌트의 렌더링 순서에 의존해서는 안 됩니다.
- 렌더링에 사용하는 입력 중 어떤 것도 변경해서는 안 됩니다. 이에는 프롭, 상태, 컨텍스트가 포함됩니다. 화면을 업데이트하려면 기존 객체를 변경하는 대신 상태를 "설정"해야 합니다.
- 컴포넌트의 논리를 반환하는 JSX로 표현하기 위해 노력하세요. "사물을 변경해야" 하는 경우 대부분 이벤트 핸들러에서 수행하고자 할 것입니다. 마지막 수단으로 useEffect를 사용할 수는 있지만, 가능한 경우 렌더링 자체로 로직을 표현해보세요.
- 순수 함수를 작성하는 것은 연습이 필요하지만 리액트 패러다임의 힘을 발휘할 수 있습니다.