목차
Side Effect란?
코드가 의도한 주된 효과 외에 추가적으로 발생하는 부수 효과를 말한다.
리액트에서는 현재의 컴포넌트에 직접적인 영향을 미치지 않는 작업이라고 한다.
현 컴포넌트 랜더링의 과정에서 필요하지만 직접적이고 즉각적으로 영향을 미치지 않는다.
sideEffect 예시
컴포넌트 함수의 주된 목적은 랜더링이 가능한 jsx 코드를 반환하는 것인데
아래코드는 좌표를 가져오고 정렬하는 것이므로 직접적인 영향이 없어 sideEffect임을 알 수 있다.
//side effect
navigator.geolocation.getCurrentPosition((position) => {
const sortedPlaces = sortPlacesByDistance(
AVAILABLE_PLACES,
position.coords.latitude,
position.coords.longitude
);
});
그래서 부수효과를 다음과 같이 작성해 보았다.
그런데 실행해보면 무한루프에 빠지는 것을 볼 수 있다.
Main.jsx
import Places from "./components/Places.jsx";
import { AVAILABLE_PLACES } from "./data.js";
import { sortPlacesByDistance } from "./loc.js";
function Main() {
const [availablePlaces, setAvailablePlaces] = useState([]);
//side effect
navigator.geolocation.getCurrentPosition((position) => {
const sortedPlaces = sortPlacesByDistance(
AVAILABLE_PLACES,
position.coords.latitude,
position.coords.longitude
);
//무한 루프 발생
setAvailablePlaces(sortedPlaces);
});
return (
<>
<main>
</main>
</>
);
}
export default Main;
무엇이 문제일까? 바로 setAvailablePlaces를 하면서 Main함수를 다시 실행하는데
그 과정에서 반복적인 호출이 일어나는 것이다. 그래서 이러한 문제를 해결하기 위해서 useEffect가 필요하다.
useEffect란?
매번 컴포넌트가 렌더링 될 때 특정 조건에 의존하여 수행되며,
컴포넌트가 최대한 순수 함수를 유지할 수 있도록 도와주는 함수
useEffect의 규칙
반복문, 조건문 혹은 중첩된 함수 내에서 Hook을 호출하면 안된다.
Main.jsx
import Places from "./components/Places.jsx";
import { AVAILABLE_PLACES } from "./data.js";
import { sortPlacesByDistance } from "./loc.js";
function Main() {
const [availablePlaces, setAvailablePlaces] = useState([]);
//side effect
//실행 시점: 앱 컴포넌트의 모든 함수가 실행된 이후
//의존성 배열이 비었을 경우 한번만 실행
useEffect(()=>{
navigator.geolocation.getCurrentPosition((position) => {
const sortedPlaces = sortPlacesByDistance(
AVAILABLE_PLACES,
position.coords.latitude,
position.coords.longitude
);
setAvailablePlaces(sortedPlaces);
});
}, []);
return (
<>
<main>
</main>
</>
);
}
export default Main;
APP컴포넌트 안에 들어갈 다음 코드를 실행하면 무한 루프가 일어날까?
아래 코드는 사용자가 아이템을 클릭할때 일어나기 때문에 무한 루프가 일어나지 않는다.
따라서 모든 부수효과에 useEffect가 필요한 것은 아니다.
function handleSelectPlace(id) {
setPickedPlaces((prevPickedPlaces) => {
if (prevPickedPlaces.some((place) => place.id === id)) {
return prevPickedPlaces;
}
const place = AVAILABLE_PLACES.find((place) => place.id === id);
return [place, ...prevPickedPlaces];
});
//이 부분을 useEffect로 감싸주어야 할까?
// -> 규칙 위반 + 어짜피 필요없음
const storeIds = JSON.parse(localStorage.getItem("selectedPlaces") || []);
if (storeIds.indexOf(id) === -1) {
localStorage.setItem("selectedPlaces", JSON.stringify([id, ...storeIds]));
}
}
그렇다면 다음의 코드는 useEffect가 필요할까?
필요가 없다. 그 이유는 아래 코드는 동기적으로 작동하고 즉각적으로 이루어지기 때문이다.
import Places from "./components/Places.jsx";
import { AVAILABLE_PLACES } from "./data.js";
import { sortPlacesByDistance } from "./loc.js";
function Main() {
const [pickedPlaces, setPickedPlaces] = useState([]);
//side effect
//실행 시점: 앱 컴포넌트의 모든 함수가 실행된 이후
//의존성 배열이 비었을 경우 한번만 실행
useEffect(() => {
const storeIds = JSON.parse(localStorage.getItem("selectedPlaces")) || [];
const storedPlaces = storeIds.map((id) =>
AVAILABLE_PLACES.find((place) => id === place.id)
);
setPickedPlaces(storedPlaces);
}, []);
return (
<>
<main>
</main>
</>
);
}
export default Main;
따라서 아래와 같이 수정하는게 적절하다.
import Places from "./components/Places.jsx";
import { AVAILABLE_PLACES } from "./data.js";
import { sortPlacesByDistance } from "./loc.js";
//밖으로 뺴어주어 굳이
const storeIds = JSON.parse(localStorage.getItem("selectedPlaces")) || [];
const storedPlaces = storeIds.map((id) =>
AVAILABLE_PLACES.find((place) => id === place.id)
);
function Main() {
const [pickedPlaces, setPickedPlaces] = useState(storedPlaces);
return (
<>
<main>
</main>
</>
);
}
export default Main;
브라우저 API 싱크를 위한 useEffect 사용
import { useRef, useState } from "react";
import Modal from "./components/Modal.jsx";
function Main() {
const [modalIsOpen, setModalIsOpen] = useState(false);
function handleRemovePlace() {
setPickedPlaces((prevPickedPlaces) =>
prevPickedPlaces.filter((place) => place.id !== selectedPlace.current)
);
setModalIsOpen(false);
const storeIds = JSON.parse(localStorage.getItem("selectedPlaces")) || [];
localStorage.setItem(
"selectedPlaces",
JSON.stringify(storeIds.filter((id) => id !== selectedPlace.current))
);
}
return (
<>
<Modal open={modalIsOpen}>
<DeleteConfirmation
onCancel={handleStopRemovePlace}
onConfirm={handleRemovePlace}
/>
</Modal>
</>
);
}
export default Main;
아래와 같이 코드를 작성하면 오류가 난다.
그 이유는 dialog의 초기 값이 null값이고 아직 ref로 초기화가 되기 전 이기 때문이다.
따라서 useEffect를 통해 해결할 수 있다. useEffect는 컴포넌트 함수가 만들어진 후에 실행하기 때문이다.
이해가 안간다면 아래를 참고하자
https://be-senior-developer.tistory.com/145
import { useRef } from 'react';
import { createPortal } from 'react-dom';
function Modal({ children, open }) {
//초기 값이 null이기 때문에 오류가 발생한다.
const dialog = useRef();
if(open){
dialog.current.showModal();
}
else{
dialog.current.close();
}
return createPortal(
<dialog className="modal" ref={dialog}>
{children}
</dialog>,
document.getElementById('modal')
);
};
export default Modal;
따라서 아래와 같이 수정하는 것이 올바른 코드이다.
import { useRef } from 'react';
import { createPortal } from 'react-dom';
import { useEffect } from 'react';
function Modal({ children, open }) {
const dialog = useRef();
useEffect(()=>{
if(open){
dialog.current.showModal();
}
else{
dialog.current.close();
}
}, [open])
return createPortal(
<dialog className="modal" ref={dialog}>
{children}
</dialog>,
document.getElementById('modal')
);
};
export default Modal;
useEffect와 함께 useCallback을 써야할 때
의존성 배열이 함수일 경우
컴포넌트의 재실행 시 함수는 객체롤 취급되어 새로 생성되면 이전 함수와는 다르다고 판단하기 때문에 무한루프에 빠질 수 있기 때문이다. 따라서 useCallback을 통해 함수를 기억해놓아 재실행을 막을 수 있다.
import { useRef, useState } from "react";
import Modal from "./components/Modal.jsx";
function Main() {
const [modalIsOpen, setModalIsOpen] = useState(false);
//useCallback을 통해 함수를 한번만 만들어 주고 재사용한다.
const handleRemovePlace = useCallback(function () {
setPickedPlaces((prevPickedPlaces) =>
prevPickedPlaces.filter((place) => place.id !== selectedPlace.current)
);
setModalIsOpen(false);
const storeIds = JSON.parse(localStorage.getItem("selectedPlaces")) || [];
localStorage.setItem(
"selectedPlaces",
JSON.stringify(storeIds.filter((id) => id !== selectedPlace.current))
);
}, []);
return (
<>
<Modal open={modalIsOpen}>
<DeleteConfirmation
onCancel={handleStopRemovePlace}
onConfirm={handleRemovePlace}
/>
</Modal>
</>
);
}
export default Main;
import { useEffect } from "react";
export default function DeleteConfirmation({ onConfirm, onCancel }) {
useEffect(()=>{
const timer = setTimeout(()=>{
console.log("SET");
onConfirm();
}, 3000);
//useEffect가 작동되기 바로 직전에 실행
//의존성이 함수일 경우 까다로움
//main컴포넌트가 재실행되면 새로운 함수 객체를 전달 받아서 무한루프에 빠질 수 있다.
//함수 객체를 새로 만들어서 다르다고 판단하기 때문에 계속 실행
return ()=>{
console.log("cleaning up timer");
clearTimeout(timer);
}
}, [onConfirm])
return (
<div id="delete-confirmation">
<h2>Are you sure?</h2>
<p>Do you really want to remove this place?</p>
<div id="confirmation-actions">
<button onClick={onCancel} className="button-text">
No
</button>
<button onClick={onConfirm} className="button">
Yes
</button>
</div>
</div>
);
}
'코딩 정보 > React' 카테고리의 다른 글
[React] 랜더링 최적화 방법을 자세히 알아보자 (0) | 2024.07.25 |
---|---|
[React] useEffect와 관련해 더 개념을 알아보자 (0) | 2024.07.23 |
[React] 랜더링의 동작과 관련 개념을 자세히 알아보자 (1) | 2024.07.17 |
[React] useReduce의 간략한 사용방법을 알아보자 (0) | 2024.07.14 |
[React] context api를 이용해서 props drilling 막기 (0) | 2024.07.14 |