몇몇 자바스크립트 함수는 순수해요. 순수 함수는 계산만을 수행하고 더 이상 무언가 하지 않아요. 컴포넌트를 순수 함수로만 엄격히 작성하면 코드베이스가 커지면서 전체 클래스에서 발생하는 갑작스런 버그와 예측할 수 없는 행동들을 피할 수 있어요. 이러한 이득을 얻기 위해서는 따라야하는 몇 가지 규칙이 있어요.
이 페이지에서는
- 순수성은 무엇이고 버그를 피하는데 어떻게 도움이 되는지
- 렌더링 단계에서 변경사항을
- 컴포넌트 안에서 실수를 찾기 위해 엄격한 모드를 어떻게 사용하는지
를 알아볼 거예요.
Purity: Components as formulas | 순수성 : 공식으로서의 컴포넌트
컴퓨터 과학에서 (그리고 특히 함수형 프로그래밍의 세계에서) 순수한 함수는 아래의 특성을 가진 함수예요.
- 자신의 일만 처리해요. 호출되기 전까지는 그 어떤 객체나 변수도 변경하지 않아요.
- 동일한 입력엔 동일한 출력을. 같은 입력값을 넣었을 때 순수함수는 항상 같은 결과를 반환해야해요.
여러분은 이미 순수 함수의 한 예시와 친숙할 거예요. 바로 수학 수식이에요.
y=2x
라는 식을 생각해볼게요.
만약x=2
라면 항상 y=4
예요.
만약 x=3
라면 항상 y=6
이에요.
x=3
일 때, y
가 시간이나 주식 시장의 상태에 따라 언제는 9
, 언제는 -1.5
가 되지 않아요.
만약 y=2x
고 x=3
이라면 는 언제나 6
일 거예요.
이 수식을 자바스크립트의 함수로 만든다면 아래와 같은 코드가 나올 거예요.
function double(number) {
return 2 * number;
}
위의 예시에서, double
은 순수함수예요. 만약 3
을 넣으면, 언제나 6
을 반환해요.
리액트는 이 개념을 중심으로 설계되었어요. 리액트는 사용자가 작성한 모든 컴포넌트가 순수함수라고 가정해요. 여러분이 만든 리액트 컴포넌트는 동일한 입력이 주어지면 항상 동일한 JSX를 반환해야해요.
drinker={2}
를 Recipe
에 넘기면, 언제나 2 cups of water
을 포함한 JSX를 반환해요.
drinker={4}
를 Recipe
에 넘기면, 언제나 4 cups of water
을 포함한 JSX를 반환해요.
수학의 수식 같은 거예요.
컴포넌트를 레시피라고 생각할 수 있어요. 만약 레시피를 따르고 요리 과정 중에 새로운 재료를 넣지 않는다면 만들 때마다 같은 음식을 얻을 거예요. 그 "음식"은 컴포넌트가 리액트에 렌더링을 하기 위해 제공하는 JSX에요.
Illustrated by Rachel Lee Nabors (출처: react.dev)
Side Effects: (un)intended consequences | 사이드 이펙트: 의도한(또는 그렇지 않은) 결과
리액트의 렌더링 과정은 순수해야만 해요. 컴포넌트는 JSX을 반환해야만 하고, 렌더링 전에는 이미 있는 객체나 변수를 *바꾸면 안돼요. 이런 것들은 컴포넌트를 순수하지 않게 만들어요!
이 예시는 이 규칙을 어긴 컴포넌트예요.
이 컴포넌트는 컴포넌트 외부에서 선언된 guest
변수를 읽고 써요. 이는 이 컴포넌트를 여러번 호출하는 것은 다른 JSX를 만든다는 것을 의미해요. 그리고 만약 다른 컴포넌트가 guest
를 읽으면 렌더링 되는 시기에 의존하여 거기에서도 다른 JSX를 만들 거예요. 이는 예측할 수 없어요.
우리의 수식인 y=2x
로 다시 돌아가 볼게요. 이제는 x=2
일 때, y=4
가 나온다고 확신할 수 없어요. 우리의 테스트는 실패하고 우리의 사용자는 당황할 것이고, 비행기가 하늘에서떨어질 수도 있어요. 여러분은 이런 함수가 혼란스러운 버그를 어떻게 생성하는지를 알 수 있어요.
이 컴포넌트를 prop으로 guest
를 전달하여 이 문제를 해결할 수 있어요.
반환되는 JSX는 guest
prop에만 의존하기 때문에 이제 컴포넌트는 순수해요.
일반적으로 컴포넌트가 어떤 특정한 순서로 렌더링된다고 기대할 수 없어요. 만약 y=2x
를 y = 5x
의 전에 호춣하는지, 후에 호출하는지는 중요하지 않아요. 두 수식은 각각 독립적으로 해결돼요. 같은 방법으로, 각각의 컴포넌트는 "자기 자신 만을 생각"해요. 그리고 렌더링하는 동안 다른 컴포넌트와 협력하거나 다른 컴포넌트에 의존하지 않아요. 렌더링은 학교 시험과 같아요. 각 컴포넌트는 JSX를 스스로 계산해야만 해요!
엄격한 모드로 순수하지 않은 계산 탐지하기
아직 이 계산을 사용하지 않았더라도 리액트에는 렌더링 중에도 읽을 수 있는 세 종류의 입력값이 있어요. 바로 props와 상태와 컨텍스트에요. 이 입력값들은 읽기 전용으로 취급돼요.
사용자의 입력에 따라 무언가 바꾸고 싶다면 변수에 넣는 대신 상태로 설정하세요. 미리 만들어진 변수가 객체는 컴포넌트를 렌더링하는 동안 절대 바꾸면 안돼요.
리액트는 개발 환경에서 모든 컴포넌트의 함수를 두 번씩 호출하는 "엄격한 모드"를 제공해요. 컴포넌트 함수를 두 번씩 호출하여 엄격한 모드는 컴포넌트가 이 규칙을 어겼는지를 탐색해요.
어떻게 원래 예시가 "Guest #1", "Guest #2", "Guest #3" 대신에 "Guest #2", "Guest #4", "Guest #6"를 보여주는지를 알아보세요. 기존의 함수는 순수하지 않기 때문에 두 번 호출하면 망가졌어요. 그러나 수정된 순수함수는 매 번 함수가 두 번 호출되더라도 동작해요. 순수함수는 계산만 하고 이러한 함수들을 두 번 호출해도 아무것도 변하지 않아요. double(2)
를 두 번 호출해도 반환 값이 바뀌지 않고 y = 2x를 두 번 풀더라도 y의 값이 변하지 않는 것처럼요. 동일한 입력값에는 항상 동일한 결과값이 나와요.
엄격한 모드는 프로덕션 환경에서는 영향을 미치지 않기 때문에 어플이 느려지진 않아요. 엄격한 모드를 선택하려면 루트 컴포넌트를 <React.StrictMode>
로 감싸세요. 어떤 프레임워크는 이 모드가 기본값이에요.
Local mutation: Your component’s little secret | 지역 변이: 컴포넌트의 작은 비밀
위의 예시에서는 컴포넌트가 렌더링하는 동안 기존 변수를 바꾸는 것이 문제였어요. 이를 조금 더 무섭게 말하면 "변이"라고 해요. 순수 함수는 함수 스코프의 외부나 호출 이전에 생성된 변수를 바꾸지 않아요. 순수성을 해치니까요!
그러나 렌더링동안에 생성된 객체나 변수는 바꿔도 괜찮아요. 이 예시에서는 []
배열을 생성하고, cups
변수에 할당했어요. 그리고 나서 12개의 컵을 배열 안에 push
했어요.
변수 cups
나 []
배열이 TeatGathering
함수의 바깥에서 생성되었다면 이것은 큰 문제가 되었을 거예요. 배열 안에 아이템을 넣어서 기존의 객체를 바꾸는 작업이기 때문이에요.
그러나, 동일한 렌더링동안 TeaGathering
안에서 생성하기 떄문에 문제가 되지 않아요. TeaGathering
외부의 코드에서는 어떤 일이 일어났는지 알지 못해요. 이를 "지역 변이"라고 해요. 컴포넌트의 작은 비밀과도 같은 거죠.
Where you can cause side effects | 사이드 이펙트가 발생하는 곳
함수형 프로그래밍은 순수성에 굉장히 의존하지만, 동시에 어딘가에서는 무언가가 변해야해요. 바로 이것이 프로그래밍의 핵심이에요! 화면을 업데이트하고, 애니메이션을 실행하고, 데이터를 바꾸는 것과 같은 이러한 변화들을 사이드 이펙트라고 해요. 이런 일은 렌더링동안이 아닌, "옆에서" 일어나요.
리액트에서 사이드 이펙트는 보통 이벤트 핸들러 안에 있어요. 이벤트 핸들러는 버튼을 클릭하는 것과 같이, 어떠한 액션을 수행할 때 리엑트가 실행하는 함수예요. 이벤트 핸들러가 컴포넌트 안에서 정의됨에도 불구하고 렌더링동안에는 실행되지 않아요. 따라서 이벤트 핸들러는 순수할 필요가 없어요.
만약 다른 옵션들을 전부 사용했고 사이드 이펙트에서 올바른 이벤트핸들러를 찾지 못했다면 컴포넌트에서 useEffect
를 사용하여 리턴된 JSX에 붙일 수 있어요. 이렇게 하는 것은 리액트에 렌더링 후, 즉 사이드 이펜트가 허요되는 시점에 이 함수를 실행하라고 말하는 것과 같아요. 그러나 이러한 방법은 최후의 보루예요.
가능하다면 로직을 렌더링만으로 표현해보세요. 이렇게 하면 얼마나 많은 것을 할 수 있는지 놀나게 될 거예요.
왜 리액트는 순수성을 신경쓰나요?
순수함수를 작성하는 것은 습관이자 규율이에요. 하지만 순수함수를 사용하면 그 이상으로 많은 기회를 얻을 수 있어요.
- 컴포넌트는 서버와 같이 다른 환경에서 실행될 수 있어요. 동일한 인풋에 대해 동일한 결과를 추출하기 때문에 하나의 컴포넌트는 많은 사용자 요청을 수행할 수 있어요.
- 입력값이 변하지 않은 컴포넌트의 렌더링을 건너뛰어서 성능을 향상시킬 수 있어요. 순수함수는 항상 같은 결과를 반환하기 때문에 안전하고 캐싱할 수 있어요.
- 만약 데이터가 깊은 컴포넌트 트리를 렌더링 도중에 변하면 리액트는 이미 만료된 렌더링을 끝내는데 시간을 허비하지 않고 렌더링을 재시작해요. 순수성은 안전하게 계산을 멈출 수 있도록 도와줘요.
우리가 만들고 있는 모든 새로운 리액트 특성은 순수성의 이익을 받아요. 데이터를 페칭하는 것부터 수행할 때 실행되는 애니메이션까지, 컴포넌트를 순수하게 유지하는 것은 리액트 패러다임의 힘을 발휘하도록 만들어요.
Recap | 요약
- 컴포넌트는 순수해야 한다는 것은 아래를 의미해요.
- 자신의 일만 처리해요. 호출되기 전까지는 그 어떤 기존 객체나 변수도 변경하지 않아요.
- 동일한 입력엔 동일한 출력을. 같은 입력값을 넣었을 때 순수함수는 항상 같은 JSX를 반환해야해요.
- 렌더링은 언제든 일어날 수 있기 때문에 컴포넌트는 서로 다른 렌더링 순서에 의존하지 않아요.
- 컴포넌트가 렌더링에 사용하는 어떤 입력값도 바꾸지 마세요. 여기서 입력값은 props, 상태 그리고 컨텍스트를 포함해요. 화면을 업데이트하려면 기존 객체를 변이하는 대신 상태를 "설정"하세요.
- 컴포넌트의 로직을 반환한 JSX 내부에서 표현하도록 노력하세요. "무언가를 바꿔야"한다면 이벤트 핸들러 안에서 수행하세요. 최후의 보루로서
useEffect
를 이용할 수 있어요. - 순수함수를 작성하려면 연습이 필요하지만 리액트 패러다임의 힘을 발휘할 수 있어요.
Challenge | 도전 과제
1. 고장난 시계 고치기
이 컴포넌트는 <h1>
의 CSS 클래스를 자정부터 아침 6시까지는 "night"
로, 다른 시간대는 "day"
로 설정했어요. 그러나 동작하지 않아요. 이 컴포넌트를 고칠 수 있나요?
여러분의 해답이 동작하는지를 확인하려면 컴퓨터의 시간설정을 변경하세요. 만약 현재 시각이 자정에서 아침 6시 사이라면, 시계는 색상을 바꿔야해요!
렌더링은 계산이에요. 무언가를 "할" 수 없어요. 같은 아이디어를 다르게 표현할 수 있나요?
이 컴포넌트는 className
을 계산하고 렌더링 결과에 해당 변수를 포함시키면 잘 작동해요.
이 예시에서 사이드 이펙트(DOM 수정)는 전혀 필요하지 않아요. 오직 JSX만 리턴하면 돼요.
2. 고장난 프로필 고치기
두 개의 Profile
컴포넌트는 다른 데이터를 가지고 나란히 렌더링돼요. 첫 번째 프로필의 "Collapse"를 누르고 그것을 "Expand" 해보세요. 양쪽 프로필은 이제 같은 사람으로 보일 거예요. 이것이 바로 버그예요.
버그의 원인을 찾고 해결해보세요.
버그를 발생시킨 코드는 Profile.js
에 있어요. 위에서부터 아래로 찬찬히 읽어보세요!
Profile
컴포넌트는 currentPerson
이라는 기존 변수를 덮어쓰고 Header
와 Avatar
컴포넌트는 이 변수에서 읽어와요. 이러한 작동은 이 세 개의 컴포넌트 모두가 순수하지 못하고 예측할 수 없게 만들어요.
버그를 고치기 위해서 currentPerson
변수를 제거하세요. 대신 모든 정보는 prop로 Profile
에서 Header
와 Avatar
에 넘기세요. 두 컴포넌트에 person
prop을 추가하고 해당 prop을 아래로 내리면 돼요.
리액트는 컴포넌트 함수가 어떤 특정한 순서로 실행된다는 것을 보장하지 않기 떄문에 변수를 정해서 그 컴포넌트 간 소통을 할 수 없다는 점을 기억하세요. props을 통해야만 소통할 수 있어요.
3. 망가진 스토리 트레이 고치기
여러분 회사의 CEO가 온라인 시계 어플에 "이야기"를 추가하기를 요청했어요. 그리고 여러분은 안된다고 할 수 없어요. 그래서 여러분은 stories
의 목록을 받아서 "Create Story"라는 플레이스홀더를 뒤에 붙이는 StoryTray
컴포넌트를 만들었어요.
"Create Story" 플레이스홀더는 prop으로 받는 stories
배열의 뒤에 가짜 이야기를 하나 만들어넣어서 구현했어요. 하지만 여러 이유로 "Create Story"는 한 번보다 더 많이 보여요. 이 문제를 고쳐보세요.
시계가 업데이트 될 때마다 "Create Story"는 두 번 추가된다는 점에 주목하세요. 이는 렌더링을 하는 동안 변이가 일어난다는 힌트를 제공해줘요. 엄격한 모드는 이러한 이슈가 더 잘 보이도록 컴포넌트를 두 번 호출해요.
StoryTray
함수는 순수하지 않아요. (prop으로) 전달받은 stories
배열에 push
를 호출하여 StoryTray
가 렌더링을 시작하기 *전에 *이미 생성되어있던 객체를 바꿔요. 이러한 코드는 버그를 만들고 예측하기 굉장히 어렵게 만들어요.
가장 간단한 해결 방법은 배열을 전혀 건들지 않고 "Create Story"를 분리해서 렌더링하는 것이에요.
아이템을 배열에 넣기 전에 (기존의 배열을 복사하여) 새로운 배열을 만드는 방법도 있어요.
이런 방법을 사용하면 변이는 지역적으로, 렌더링 함수는 순수하게 유지할 수 있어요. 그러나 조심해야해요. 예를 들어서, 만약 배열에 있던 기존 아이템을 바꾸면 이 아이템들도 복사해야만 해요.
어떤 연산자가 배열을 변화시키고, 어떤 연산자가 그렇지 않은지를 기억하면 유용해요. push
, pop
, reverse
그리고 sort
는 기존 배열을 변화시켜요. 하지만 slice
, filter
그리고 map
은 새로운 배열을 생성해요.
'리액트 공식문서 | React Docs > Learn > Learn React' 카테고리의 다른 글
[Adding Interactivity] Adding Interactivity Overview | 상호작용 추가하기 개요 (0) | 2024.02.15 |
---|---|
[Describing the UI] Understanding Your UI as a Tree | 트리로 UI 이해하기 (1) | 2024.02.15 |
[Describing the UI] Rendering Lists | 리스트 렌더링하기 (0) | 2024.02.14 |
[Describing the UI] Conditional Rendering | 조건부 렌더링 (1) | 2024.02.13 |
[Describing the UI] Passing Props to a Component | 컴포넌트에 props 전달하기 (2) | 2024.02.13 |