안녕하세요 웹파트 YB 김다현입니다 😊
이번 2주차에는 조건부 렌더링, 리스트 렌더링, 컴포넌트를 순수하게 유지하기, 트리로서의 UI 이렇게 총 네 개의 챕터를 읽고 공부해봤는데요! 조건부 렌더링과 리스트 렌더링은 나름 빠르게 이해한 반면 순수 컴포넌트는 이해하는 데 조금 시간도 걸렸고, 아직 이해하지 못한 부분도 있는 것 같아 이번 아티클에서 다뤄보며 더 공부해보려고 해요 ⊹꒰⍢⑅ ꒱꙳ (트리 UI도 기회가 된다면 추가를...)
그리고 저는 사실 리액트 개념이 부족해서... 우선 공식문서를 최대한 쉽게 풀어내서 설명하는 거에 초점을 맞춰서 작성해볼게요!
❄️ 순수 컴포넌트
리액트에서 순수한 컴포넌트라는 건 항상 같은 입력이 들어오면 같은 결과를 보여주는 컴포넌트예요.
커피를 만들 때 레시피가 있다면 같은 재료를 넣을 때마다 항상 같은 맛의 커피가 나오겠죠?! 이 개념이 바로 순수 컴포넌트예요.
function double(number) {
return 2 * number;
}
공식 문서에 있는 예시 코드를 들고 와봤는데요! 지금 이 함수 double 에서는 숫자를 넣으면 항상 2배로 돌려주게 되어있죠?
3을 넣으면 항상 6이 나오고, 5를 넣으면 항상 10이 나와요. 이 함수는 같은 입력에 대해 항상 같은 결과를 보여주기 때문에 순수하다고 볼 수 있어요.
사이드 이펙트?
리액트에서는 컴포넌트가 자기 자신만의 입력에만 의존해서 작동해야 한다고 해요. 그런데 만약 컴포넌트가 외부 변수나 객체를 변경해버리면 예측하기 어려운 결과가 나타날 수 있겠죠? 이렇게 컴포넌트가 원래 계획에 없던 행동을 하는 걸 사이드 이펙트라고 불러요. (정확히 말하자면 렌더링과 직접 관련이 없는 작업 즉, "렌더링 외부" 에서 발생하는 변화를 말한다고 해요!)
let guest = 0;
function Cup() {
// 지금 여기서 외부 변수를 변경하고 있죠?!
guest = guest + 1;
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaSet() {
return (
<>
<Cup />
<Cup />
<Cup />
</>
);
}
여기서 Cup 컴포넌트는 외부 변수 guest를 계속해서 바꾸고 있어요. 리액트가 여러 번 렌더링할 때마다 guest 값이 변해서, 각 호출마다 다른 JSX를 반환하게 돼요. 근데 이렇게 외부 변수를 계속 바꾸면 어떤 문제가 생기냐!
1️⃣ 결과를 예측할 수 없어짐! guest는 컴포넌트가 호출될 때마다 값이 변하니까 같은 컴포넌트를 다시 불러도 결과가 달라져요.
2️⃣ 다른 컴포넌트에도 영향을 끼침! 만약 guest 변수를 다른 컴포넌트에서 사용했다면? 그 컴포넌트도 guest 값이 언제 어떻게 변했는지에 따라 다른 결과를 보여줄 수 있겠죠...
이런 문제들이 생기기 때문에 리액트는 한 번 렌더링될 때마다 같은 입력에 대해 같은 출력을 보여주길 기대해요. 외부 변수를 바꾸는 컴포넌트는 규칙을 어기게 됩니다 ❌
Props 관리를 통해 해결!
리액트에서는 이렇게 예측 불가능한 상황을 막기 위해 컴포넌트가 외부 변수를 직접 변경하는 게 아니라 오직 props에만 의존하도록 권장해요. props를 통해 필요한 값을 전달하면 항상 같은 입력이 들어올 떄 같은 결과가 나오겠죠?!
function Cup({ guest }) {
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaSet() {
return (
<>
<Cup guest={1} />
<Cup guest={2} />
<Cup guest={3} />
</>
);
}
이렇게 고치면 이제 Cup 컴포넌트는 외부 변수를 사용하지 않게 되고, props로 guest 값을 전달 받아서 항상 같은 입력에 대해 같은 JSX를 반환하게 돼요.
지역 변형: 컴포넌트의 작은 비밀
저는 이 부분이 제일 이해하기 어려웠는데요... 짧게 요약하자면 컴포넌트 내부에서 만들어진 변수는 바꿔도 괜찮지만 컴포넌트 바깥에 있는 변수는 바꾸면 안 된다는 뜻이더라구요! 지역 변형과 사이드 이펙트의 차이점을 이해하는 게 핵심인 거 같아요 😮💨
그러나, 렌더링하는 동안 그냥 만든 변수와 객체를 변경하는 것은 전혀 문제가 없습니다.
- 리액트 공식문서 中 -
리액트 컴포넌트가 렌더링 될 때, 컴포넌트 내부에서 필요한 변수를 만들고 그 변수를 변경하는 건 문제가 없어요!
예를 들자면 컴포넌트 안에서만 쓰이는 변수를 만들고, 그 변수에 무언가를 추가하는 코드가 있다면 이건 문제가 되지 않아요.
왜냐하면 이 변수는 컴포넌트가 렌더링 될 때만 만들어지고 렌더링이 끝나면 사라지기 때문이에요. 이런 방식의 변수 변경을 지역 변형이라고 한답니다.
function Cup({ guest }) {
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaGathering() {
// 지역 변형: 렌더링할 때마다 새로운 cups 배열이 만들어짐
let cups = [];
for (let i = 1; i <= 12; i++) {
cups.push(<Cup key={i} guest={i} />);
}
return cups;
}
여기서 cups라는 배열은 TeaGathering 컴포넌트가 렌더링될 때마다 새로 생성돼요. for문을 통해 Cup 컴포넌트를 배열에 push하고 있지만, 이 cups 배열은 컴포넌트 안에서만 사용되기 때문에 외부에는 영향을 주지 않겠죠...?! 컴포넌트 내부에서만 변하는 값이기 때문에 다른 컴포넌트가 이 변화를 알 필요가 없어요. 그래서 이렇게 컴포넌트 내부에서만 사용되는 변형은 문제가 없답니다!
사이드 이펙트가 필요할 땐 어떡해?!
이 챕터의 마지막 섹션은 '부작용을 일으킬 수 있는 지점' 인데요. 되게 중요한 부분인 거 같은데... 저한텐 어렵게 느껴졌어서 더 꼼꼼히 공부해봤어요 😭
먼저 사이드 이펙트는 위에서 설명했던 것처럼 렌더링과 과정과 관련이 없는 작업을 뜻하는데요. 말 그대로 부수적인 효과예요.
화면 업데이트, 애니메이션 시작, 서버에서 데이터 불러오기 등등 모두 사이드 이펙트겠죠! 근데 아까 분명 위에서 사이드 이펙트가 발생하면 컴포넌트의 순수성이 깨진다고 했지만... 사실 개발을 하다보면 꼭 필요한 경우가 있어요. 이럴 때 어떻게 해야하는지를 다루는 섹션이었어요!
1) 이벤트 핸들러를 통한 사이드 이펙트
React에선, 사이드 이펙트는 보통 이벤트 핸들러에 포함됩니다.
- 리액트 공식문서 中 -
이벤트 핸들러는 사용자의 액션이 있을 때 특정 작업을 실행하게 해요. 예를 들어 버튼을 클릭할 때 데이터가 저장된다거나, 애니메이션이 시작된다거나...! 이벤트 핸들러는 컴포넌트가 렌더링되는 동안에는 실행되지 않고, 이벤트가 발생했을 때만 실행돼요. 그래서 렌더링 자체에는 영향을 주지 않기 때문에 컴포넌트의 순수성을 유지할 수 있답니다!
function TeaCup() {
function handleClick() {
alert('Dahyun!'); // 클릭 시 발생하는 사이드 이펙트
}
return <button onClick={handleClick}>Drink Tea</button>;
}
여기서 handleClick은 클릭할 때만 실행돼요. 렌더링 도중에 실행되지 않고, 오직 이벤트가 발생했을 때만 실행된다는 뜻! 그래서 handleClick이 부수적인 작업을 처리해도 컴포넌트의 순수성에는 영향을 주지 않아요.
2) useEffect를 사용한 사이드 이펙트 관리
다른 옵션을 모두 사용했지만 사이드 이펙트에 적합한 이벤트 핸들러를 찾을 수 없는 경우에도, 컴포넌트에서 useEffect 호출을 사용하여 반환된 JSX에 해당 이벤트 핸들러를 연결할 수 있습니다.
- 리액트 공식문서 中 -
그런데 사이드 이펙트에 적합한 이벤트 핸들러를 찾을 수 없는 경우도 분명 생기겠죠?
예를 들면 컴포넌트가 화면에 처음 나타났을 때 데이터를 불러와야 하는 경우에는 이벤트 핸들러로 해결하기 어려워요. 이럴 땐 useEffect를 사용하여 해결할 수 있어요.
useEffect는 리액트에게 "이 작업은 컴포넌트가 렌더링된 후에 실행해야 한다"고 알려주는 기능이라고 할 수 있어요.
import { useEffect } from 'react';
function DataFetcher() {
useEffect(() => {
// 렌더링 후에 실행될 사이드 이펙트
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data));
}, []);
return <div>Loading data...</div>;
}
이 코드로 보면, 컴포넌트가 화면에 나타난 후 fetch 요청을 보내서 데이터를 가져오는 예시 코드인데요!
렌더링 도중이 아니라 렌더링이 완료된 후에 실행되기 때문에 컴포넌트는 순수성을 유지할 수 있어요.
🤔 왜 useEffect는 최후의 수단이지?
그러나 이 접근 방식이 마지막 수단이 되어야 합니다.
- 리액트 공식문서 中 -
공식문서에서도 이렇게 말하고 있고 저도 useEffect를 많이 쓰는 건 안 좋다? 이런 말을 스쳐지나가면서 들은 기억이 있었는데요.
대체 왜! 마지막 수단으로 써야 한다는 건지 궁금해서 알아봤어요.
1️⃣ 렌더링 성능에 영향을 끼침
useEffect를 남발하면 렌더링 이후에도 추가 작업이 계속 발생해요. 이 작업들은 브라우저가 컴포넌트를 화면에 그리는 작업과 별도로 일어나기 때문에 성능에 영향을 줄 수 있다고 해요. 그리고 useEffect가 너무 많으면 렌더링 흐름이 예측하기 어려워지거나 느려질 수 있다고 하네요!
2️⃣ 코드 복잡성 증가
useEffect는 특정 조건에 따라 부수적인 작업을 실행하는 훅이라 많이 사용할수록 컴포넌트의 동작을 이해하기 어려워질 수 있어요. useEffect 안에서 상태를 바꾸는 작업이 여러 번 일어나면 컴포넌트가 예상치 않게 여러 번 재렌더링 될 수 있고, 코드가 복잡해져 버그 발생 가능성도 높아진다고 합니다!
3️⃣ 의도치 않은 재렌더링 유발
useEffect는 기본적으로 렌더링 후 실행되는데 종종 상태나 props가 변경될 때마다 재실행 될 수 있어요. 이로 인해 필요하지 않는 재렌더링이 발생할 수 있어요.
우선 제가 알아본 이유는 이 정도인데 혹시 알고 계신 다른 이유가 있다면 공유해주세요... ㅎㅎ
🤔 궁금한 점
근데 사이드 이펙트를 관리하기 위한 방법이 이 두 개밖에 없는지?! 더 최적의 방법은 없는 건지... 아니면 다른 방법들도 있는지가 궁금해지네요
'나야, 리액트 스터디' 카테고리의 다른 글
[week 2] - 조건부 렌더링, 리스트 렌더링, 컴포넌트를 순수하게 유지하기, 트리로서의 UI (5) | 2024.11.03 |
---|---|
[week2] 순수 컴포넌트, 사이드 이펙트, 트리로서 UI (5) | 2024.11.03 |
[week2] - 조건부 렌더링, 리스트 렌더링, 컴포넌트를 순수하게 유지하기, 트리로서의 UI (2) | 2024.11.03 |
[week 1] 리액트 컴포넌트와 JSX (4) | 2024.10.27 |
[week1] 컴포넌트 딥다이브 🌊 (4) | 2024.10.27 |