function Home() {
const userData = JSON.parse(sessionStorage.getItem('user'))
//const { currentUser } = useContext(UserContext);
const [contents, setContents] = useState([]);
useEffect(() => {
const dbContents = query(
collection(db, "contents")
//orderBy("createdAt", "desc")
);
onSnapshot(dbContents, (snapshot) => {
const contentsArr = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
setContents(contentsArr);
});
},[]);
//useEffect의 두 번째 매개변수로 []을 넣어줘야 무한 렌더링에서 빠져나온다
return (
<>
<TextFactory />
{contents.map((content) => (
<Texts
key={content.id}
content={content}
isOwner={content.creatorId === userData.uid}
/>
))}
</>
);
}
export default Home;
위 코드인 Home 컴포넌트와 하위의 TextFactory, Texts까지 모두 무한 렌더링이 일어났다. 찾아보니 useEffect와 setState 두 개와 관련있는 것 같았는데, 내가 useEffect의 개념을 아직 제대로 모르는 것 같아서 정리하려고 한다.
Effects let you specify side effects that are caused by rendering itself, rather than by a particular event. Sending a message in the chat is an event because it is directly caused by the user clicking a specific button. However, setting up a server connection is an Effect because it should happen no matter which interaction caused the component to appear. Effects run at the end of a commit after the screen updates. This is a good time to synchronize the React components with some external system (like network or a third-party library).
📌 Effect를 활용해 특정 이벤트가 아닌 렌더링에 의해 일어나는 사이드 이펙트를 특정할 수 있다.
📌 서버와의 연결을 구축하는 등의 일은 버튼이나 제출과 상관없이 일어나야 하는 일이다. -> 그래서 이것은 event가 아니고 effect인 것
📌 Effect는 스크린이 업데이트 된 후 commit의 마지막 단계에서 발생한다. 바로 이 때에 컴포넌트를 외부 시스템과 동기화시키기 좋음.
컴포넌트들이 화면에 출력되기 전에, 이들은 리액트에 의해 렌더링이 먼저 되어야 한다.
컴포넌트가 주방에서 요리하는 요리사라고 가정해보자. 그리고 리액트는 주문과 음식을 전달하는 웨이터이다. 손님에게 주문을 받은 후 완성된 요리를 전달하기까지의 과정을 이러할 것이다.
- Triggering a render (delivering the guest’s order to the kitchen) 리액트가 렌더를 일으킨다.
= 웨이터가 손님의 주문을 확인하고 주방에 전달한다.- Rendering the component (preparing the order in the kitchen) 컴포넌트를 렌더링한다.
= 주방에서 요리사가 음식을 만든다.- Committing to the DOM (placing the order on the table) DOM에 컴포넌트를 출력한다.
= 손님에게 음식을 가져다준다.
1) 컴포넌트의 초기 렌더링일 때
import Image from './Image.js';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'))
root.render(<Image />);
createRoot의 render메소드를 통해 컴포넌트 초기 렌더링을 한다.
2) 해당 컴포넌트, 혹은 조상 컴포넌트에서 state 변경이 감지되었을 때
: 컴포넌트의 state를 변경하는 것은 자동으로 리렌더링을 일으킨다.
(식당에서 식사를 끝낸 손님이 디저트나 음료를 추가 주문하는 것을 생각해보라. 이렇게 하면 웨이터는 앞서 했던 것처럼, 주문을 주방에 전달하고 다시 손님에게 음료나 디저트를 서빙하러 가야한다.)
렌더를 trigger한 후에, 리액트는 컴포넌트를 호출하여 화면에 어떤 걸 나타낼 것인지 정리한다.
초기 렌더링의 경우, 리액트는 root 컴포넌트를 부를 것이다.
리렌더링의 경우, 리액트는 상태가 변경된 컴포넌트를 부를 것이다.
-> 이 과정은 반복적이다. 업데이트된 컴포넌트가 다른 컴포넌트를 배출할 경우, 리액트는 그 컴포넌트를 다음에 렌더링한다. 이것을 더 이상 감싸진 컴포넌트가 없을 때까지 반복한다.
export default function Gallery() {
return (
<section>
<h1>Inspiring Sculptures</h1>
<Image />
<Image />
<Image />
</section>
);
}
function Image() {
return (
<img
src="https://i.imgur.com/ZF6s192.jpg"
alt="'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals"
/>
);
}
초기 렌더링에서, 리액트는 <section>,<h1>,<img>의 DOM node들을 만든다.
리렌더링에서, 리액트는 상태가 바뀐 컴포넌트가 있는지 확인하고, 만약 있다면 commit을 하기 전까지 아무것도 하지 않고 기다린다.
컴포넌트들을 모두 호출한 리액트는 이제 DOM을 조작(수정)한다.
초기 렌더링에서, 리액트는 appendChild() API를 이용해 모든 DOM node들을 화면에 나타낸다.
리렌더링에서, 리액트는 DOM이 마지막 렌더링 결과와 매치되도록 필요한 최소한의 동작을 한다.
리액트는 렌더들 간 차이가 있을 때에만 DOM node 를 변경한다.
export default function Clock({ time }) {
return (
<>
<h1>{time}</h1>
<input />
</>
);
}
위 코드에서, 매 초마다 time props가 변경되어 리렌더링 되지만, input창과 그 안의 텍스트는 사라지지 않는다.
이것은 commit을 하는 동안, 리액트는 <h1>만 업데이트하기 때문이다. 마지막 렌더링과 비교했을 때 input태그는 같은 자리에 있기 때문에, 리액트는 이것을 건드리지 않는다.
리액트의 모든 행위가 끝나면, 브라우저는 화면을 "리페인트"한다!
결국 useEffect가 발동하는 시점은 3단계인 commit의 마지막 지점이라는 것이다.
컴포넌트가 리렌더링될 때마다, useEffect는 화면에 DOM들을 흩뿌리고 나서(committing) 그 다음에 실행될 것이다.
즉 useEffect 안에 들어있는 코드는 렌더링 결과가 화면에 출력될 때까지 실행이 딜레이된다
import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
if (isPlaying) {
ref.current.play(); // Calling these while rendering isn't allowed.
} else {
ref.current.pause(); // Also, this crashes.
}
return <video ref={ref} src={src} loop playsInline />;
}
위 코드의 VideoPlayer 컴포넌트는 isPlaying을 props로 받지만, html의 video 태그에는 isPlaying이 없다. 따라서 자체적으로 isPlaying prop을 동기화해야 한다.
그러나 위 코드는 런타임 에러가 발생한다.
렌더링 과정에서 DOM node를 조작하려 하기 때문이다. 리액트에서, 렌더링은 순수히 JSX를 계산해야하고, DOM 조작과 같은 사이드 이펙트를 포함해서는 안된다.
더욱이, VideoPlayer가 초기 콜 될 때, DOM은 아직 존재하지도 않는다. 리액트는 당신이 리턴한 JSX를 보기 전까진 어떤 DOM을 생성해야 할지 모르기 때문에, play()나 pause()를 부를 수 있는 DOM이 아직 없는 것이다.
👉 해결책이 바로 useEffect를 사용해 이 해당 사이드 이펙트를 묶어버리는 것이다! 렌더링 계산에서 아예 빠지도록!
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
});
이렇게 해주면, 렌더링을 해서 화면에 결과가 먼저 출력된 후, useEffect 부분이 실행된다.
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
});
setState는 렌더링을 발생시킨다. 위 코드에서 무한루프에 빠지는 과정은 다음과 같다.
초기 렌더링 -> Effct run -> set the state -> Effect run -> set the state ...
Effect는 디폴트로 매 렌더링 이후 실행된다. 하지만 아마 이것은 우리가 원하는 것이 아닐 것이다.
useEffect(() => {
if (isPlaying) { // It's used here...
// ...
} else {
// ...
}
}, [isPlaying]); // ...so it must be declared here!
dependecy값을 isPlaying이라고 선언해줌으로써, isPlaying값이 이전 렌더링 때와 동일하면 Effect의 실행을 스킵할 수 있게 된다.
dependecy 자리에는 두 개 이상도 들어갈 수 있고, 이 경우 이 중 하나라도 값이 변하면 Effect가 실행된다.
import { useEffect } from 'react';
import { createConnection } from './chat.js';
export default function ChatRoom() {
useEffect(() => {
const connection = createConnection();
connection.connect();
}, []);
return <h1>Welcome to the chat!</h1>;
}
export function createConnection() {
// A real implementation would actually connect to the server
return {
connect() {
console.log('✅ Connecting...');
},
disconnect() {
console.log('❌ Disconnected.');
}
};
}
ChatRoom 컴포넌트의 Effect가 어떠한 prop도 받지 않고 dependecy도 빈 array이기 때문에, 처음에 컴포넌트가 마운트될 때 한 번만 Effect가 실행되어 '✅ Connecting...'가 한 번만 출력될 것을 예상할 수 있다.
하지만 실제로 콘솔을 살펴보면 '✅ Connecting...'이 두 번 출력된다.
왜 그럴까?
사용자가 ChatRoom 페이지에서 다른 페이지로 잠시 이동한다고 생각해보자. 그 순간 ChatRoom은 언마운트된다. 하지만 그렇다고 해서 connect 해놓은 것까지 끊어지진 않는다. 따라서 유저가 ChatRoom 페이지에 재접속하면 Effect가 실행되며 connect가 한 번 더 쌓인다.
이런 식의 오류는 놓치지 쉽기 때문에, 리액트는 개발자를 위하여 개발환경에서 Effect를 마운트 한 후 모든 컴포넌트를 한 번 더 마운트한다.
콘솔에 두 번 찍힌 것을 보고, 개발자는 "아, 컴포넌트가 언마운트 되었지만 connect는 아직 유효하구나" 라고 알아차릴 수 있다.
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect();
};
}, []);
리턴값으로 cleanUp code를 작성해주면,
"✅ Connecting..."
"❌ Disconnected."
"✅ Connecting..."
라고 콘솔에 출력되는 걸 볼 수 있다. 이것은 개발환경에서 정상이다. 배포 시에는 "✅ Connecting..."가 한 번만 출력될 것이다.
공식문서는 신이야!
https://react.dev/learn/synchronizing-with-effects