import { sortPlacesByDistance } from "./loc.js";
function App() {
const [availablePlaces, setAvailablePlaces] = useState([]);
// 부수효과 => 사용자의 위치가 이 앱에 필요하긴 하지만 컴포넌트 함수의 주 목적(JSX 렌더링)과는 직접적인 관계가 없다.
navigator.geolocation.getCurrentPosition((position) => {
const sortedPlaces = sortPlacesByDistance(
AVAILABLE_PLACES,
position.coords.latitude,
position.coords.longitude
);
setAvailablePlaces(sortedPlaces); // 앱 컴포넌트 재실행 -> 계속 사용자 위치 파악을 할 것이다 => 무한 루프.
});
return (
<>
{/* ... */}
<main>
<Places
title="I'd like to visit ..."
fallbackText={"Select the places you would like to visit below."}
places={pickedPlaces}
onSelectPlace={handleStartRemovePlace}
/>
<Places
title="Available Places"
places={AVAILABLE_PLACES}
onSelectPlace={handleSelectPlace}
/>
</main>
</>
);
}
useState에 의해서 상태가 업데이트가 되고 앱 컴포넌트가 재실행된다. 컴포넌트가 재실행되면 또다시 사용자 위치 정보를 받게 되면서 무한 루프에 빠지게 된다.useEffect 훅을 배울 것이다.useEffectuseEffect를 사용하는 Side Effect(부수 효과)useEffect의 첫 인수인 함수가 리액트로 인해 실행되는 시점은 앱 컴포넌트가 실행되고 나서 즉시 실행되지 않는다. → 앱 컴포넌트 함수의 실행이 모두 완료가 된 이후에 실행된다!
JSX 코드가 반환된 이후의 시점에서야
useEffect에 전달한 Side Effect 함수가 실행된다. → 리액트는 컴포넌트 함수의 실행이 완료된 후에 부수 효과 함수를 실행한다.
의존성(배열)의 값이 변화했을 경우에 한해서 useEffect 함수를 재실행한다.
// App.jsx
import { useRef, useState, useEffect } from "react";
import { sortPlacesByDistance } from "./loc.js";
function App() {
const [availablePlaces, setAvailablePlaces] = useState([]);
useEffect(() => {
navigator.geolocation.getCurrentPosition((position) => {
const sortedPlaces = sortPlacesByDistance(
AVAILABLE_PLACES,
position.coords.latitude,
position.coords.longitude
);
setAvailablePlaces(sortedPlaces);
});
}, []);
return (
<>
{/* ... */}
<main>
<Places
title="I'd like to visit ..."
fallbackText={"Select the places you would like to visit below."}
places={pickedPlaces}
onSelectPlace={handleStartRemovePlace}
/>
<Places
title="Available Places"
places={availablePlaces}
fallbackText="Sorting places by distance..."
onSelectPlace={handleSelectPlace}
/>
</main>
</>
);
}
useEffect( 부수효과 코드를 감쌀 함수, dependency(의존성) 배열 )
[]로 비어있다. === 의존성이 없다 === 변화할 수 없다. → 리액트는 해당 Effect 함수를 재실행하지 않는다. 오직 처음으로 실행된 이후에만 딱 한번 실행된다.<Places fallbackText="Sorting places by distance..."/> : 아직 장소 정렬이 되지 않았을 때 사용자에게 보여 줄 fallback(대체) 텍스트이다.

useEffect를 사용하지 않는 이유 1useEffect의 과한 사용이나 불필요한 곳에서의 사용은 좋지 않다.import { useRef, useState, useEffect } from "react";
import { sortPlacesByDistance } from "./loc.js";
function App() {
const [availablePlaces, setAvailablePlaces] = useState([]);
useEffect(() => {
// 부수 효과 1
navigator.geolocation.getCurrentPosition((position) => {
const sortedPlaces = sortPlacesByDistance(
AVAILABLE_PLACES,
position.coords.latitude,
position.coords.longitude
);
setAvailablePlaces(sortedPlaces);
});
}, []);
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];
});
// 부수 효과2
const storedIds = JSON.parse(localStorage.getItem("selectedPlaces")) || [];
if (storedIds.indexOf(id) === -1) {
// 이미 해당되는 장소가 있는지 확인(indexOf) -> 없다면..(-1)
localStorage.setItem(
"selectedPlaces",
JSON.stringify([id, ...storedIds])
);
}
}
}
useEffect를 쓰는 것은 훅의 규칙을 위반하는 것이다.훅의 규칙 : 중첩된 함수나 if문 등에서 리액트 훅을 사용할 수 없다. => 컴포넌트의 root에서만 사용가능하다.
useEffect가 필요하지 않는다.
useEffect훅이 필요한 경우는 무한 루프를 방지하기 위해서이거나 컴포넌트 함수가 최소 한번은 실행된 이후에 작동이 가능한 코드가 있을 때 뿐이다.
useEffect를 사용하지 않는 이유 2import { useRef, useState, useEffect } from "react";
import Places from "./components/Places.jsx";
import { AVAILABLE_PLACES } from "./data.js";
import Modal from "./components/Modal.jsx";
import DeleteConfirmation from "./components/DeleteConfirmation.jsx";
import logoImg from "./assets/logo.png";
import { sortPlacesByDistance } from "./loc.js";
// 부수 효과 3-2
// 앱이 업데이트 될때마다 실행될 필요가 없으므로 컴포넌트 밖에서 작성됨.
const storedIds = JSON.parse(localStorage.getItem("selectedPlaces")) || [];
const storedPlaces = storedIds.map((id) =>
AVAILABLE_PLACES.find((place) => place.id === id)
);
function App() {
const modal = useRef();
const selectedPlace = useRef();
const [availablePlaces, setAvailablePlaces] = useState([]);
const [pickedPlaces, setPickedPlaces] = useState(storedPlaces); // 부수 효과 3-2에서 리턴된 저장된 장소 데이터들을 pickedPlaces 상태에 초기화.
/* 부수 효과 3-3
useEffect(() => {
const storedIds = JSON.parse(localStorage.getItem("selectedPlaces")) || [];
const storedPlaces = storedIds.map((id) =>
AVAILABLE_PLACES.find((place) => place.id === id)
);
setPickedPlaces(storedPlaces);
}, []);
*/
// 부수 효과 1
useEffect(() => {
navigator.geolocation.getCurrentPosition((position) => {
const sortedPlaces = sortPlacesByDistance(
AVAILABLE_PLACES,
position.coords.latitude,
position.coords.longitude
);
setAvailablePlaces(sortedPlaces);
});
}, []);
function handleStartRemovePlace(id) {
modal.current.open();
selectedPlace.current = id;
}
function handleStopRemovePlace() {
modal.current.close();
}
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];
});
// 부수 효과 2
const storedIds = JSON.parse(localStorage.getItem("selectedPlaces")) || [];
if (storedIds.indexOf(id) === -1) {
localStorage.setItem(
"selectedPlaces",
JSON.stringify([id, ...storedIds])
);
}
}
function handleRemovePlace() {
setPickedPlaces((prevPickedPlaces) =>
prevPickedPlaces.filter((place) => place.id !== selectedPlace.current)
);
modal.current.close();
// 부수 효과 3-1
const storedIds = JSON.parse(localStorage.getItem("selectedPlaces")) || [];
localStorage.setItem(
"selectedPlaces",
JSON.stringify(storedIds.filter((id) => id !== selectedPlace.current))
); // filter : 조건문이 만족되면 삭제하고자 하는 항목이 아니다 -> true 반환 => 항목을 그대로 유지
// 조건문이 맞지 않다면(id가 매치되면) 삭제하고자 하는 항목이다 -> false 반환 => 항목에서 배제.
}
}
localStorage에서 id를 불러와 filter 함수를 통해 선택한 아이템을 삭제하는 로직이다. filter와 관련된 내용은 주석에 적어 놓았다.localStorage에 저장된 id들을 불러오고 map 함수를 이용해서 저장된 모든 장소들의 데이터를 불러온다. (AVAILABLE_PLACES와 일치하는 id를 찾고 해당 데이터를 반환.)storedPlaces를 pickedPlaces 상태에 초기화한다. → 굳이 useEffect를 쓰지 않고도 업데이트 가능 + UI 표현 가능useEffect를 사용하여 이미 저장된 장소들을 표현한 것이다.[]로 했기 때문에 초기에만 실행된다.useEffect이다. → navigator는 getCurrentPosition 함수의 특성상 약간의 시간차가 발생하지만 이 부수효과 코드는 시간차 없이 즉시 실행된다. 따라서 굳이 useEffect를 쓰지 않아도 되며, 부수 효과 3-2처럼 작성해도 된다!
use Effect를 활용하는 다른 적용 사례useEffect를 이용해 모달 열고 닫기useEffect 사용하지 않고 모달 동작시키기// Modal.jsx
import { useRef } from "react";
import { createPortal } from "react-dom";
export default function Modal({ open, children }) {
const dialog = useRef();
if (open) {
dialog.current.showModal();
} else {
dialog.current.close();
}
return createPortal(
<dialog className="modal" ref={dialog}>
{children}
</dialog>,
document.getElementById("modal")
);
}
// App.jsx
import { useRef, useState, useEffect } from "react";
import Modal from "./components/Modal.jsx";
function App() {
const [modalIsOpen, setModalIsOpen] = useState(false);
function handleStartRemovePlace(id) {
setModalIsOpen(true);
selectedPlace.current = id;
}
function handleStopRemovePlace() {
setModalIsOpen(false);
}
function handleRemovePlace() {
//...
setModalIsOpen(false)
//...
}
return (
<>
<Modal open={modalIsOpen}>
{/* ... */}
</Modal>
{/* ... */}
);
}

showModal(), close() 메서드를 컴포넌트 함수의 내부에서 호출하고 있으므로 오류가 발생한다.dialog 참조는 아직 설정이 되지 않았다. → JSX 코드가 실행되기 이전이기 때문이다. 아직 연결이 안됨!dialog는 undefined 상태였다.showModal(), close() 메서드와 같은 DOM API와 속성값(혹은 상태값)이 동기화될 수 있도록 useEffect를 이용하여 JSX 코드 실행 이후(컴포넌트 이후 실행)로 해당 메서드들(showModal, close)이 실행되도록 해야한다. 그래야지 dialog 참조가 연결이 된다.useEffect 이용하기// Modal.jsx
import { useRef, useEffect } from "react";
import { createPortal } from "react-dom";
export default function Modal({ open, children }) {
const dialog = useRef();
useEffect(() => {
if (open) {
dialog.current.showModal();
} else {
dialog.current.close();
}
}, []);
// 의존성 배열에 Effect 함수가 필요로 하는 의존성을 추가해야 한다.
return createPortal(
<dialog className="modal" ref={dialog}>
{children}
</dialog>,
document.getElementById("modal")
);
}
useEffect를 사용하여 컴포넌트의 실행 이후로 부수 효과가 실행되도록 했다. 그러나! 해당 코드에는 의존성을 추가해야한다.useEffect에서 사용된다면,,)ref)나 브라우저에 구축된 객체와 메서드(ex. navigator,..)들은 의존성을 분류되지 않는다.useEffect는 컴포넌트 함수가 다시 실행되도록 하는 의존성에 대해서만 적용되기 때문이다. → 의존성이 변경될 때마다 useEffect가 동작하기 때문이다.// Modal.jsx
import { useRef, useEffect } from "react";
import { createPortal } from "react-dom";
export default function Modal({ open, children }) {
const dialog = useRef();
useEffect(() => {
if (open) {
dialog.current.showModal();
} else {
dialog.current.close();
}
}, [open]); // open 속성을 추가하여 의존성 검사
// 의존성 배열에 Effect 함수가 필요로 하는 의존성을 추가해야 한다.
// Modal : open 속성 사용하여 열고 닫고를 정한다.
return createPortal(
<dialog className="modal" ref={dialog}>
{children}
</dialog>,
document.getElementById("modal")
);
}

// App.jsx
<Modal open={modalIsOpen} onClose={handleStopRemovePlace}>
<DeleteConfirmation
onCancel={handleStopRemovePlace}
onConfirm={handleRemovePlace}
/>
</Modal>;
// Modal.jsx
import { useRef, useEffect } from "react";
import { createPortal } from "react-dom";
export default function Modal({ open, children, onClose }) {
return createPortal(
<dialog className="modal" ref={dialog} onClose={onClose}>
{/* onClose 속성 전달 */}
{children}
</dialog>,
document.getElementById("modal")
);
}
// DeleteConfirmation.jsx
export default function DeleteConfirmation({ onConfirm, onCancel }) {
setTimeout(() => {
onConfirm();
}, 3000);
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>
);
}
{ open ? { children } : null } 조건을 통하면 해결이 가능하다.import { useEffect } from "react";
export default function DeleteConfirmation({ onConfirm, onCancel }) {
useEffect(
() => {
// ==== Effect ====
const timer = setTimeout(() => {
console.log("TIMER SET");
onConfirm();
}, 3000);
// ==== cleanup ====
return () => {
console.log("Cleaning up timer");
clearTimeout(timer);
};
// ==== cleanup ====
},
// ==== Effect ====
[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>
);
}
onConfirm 함수를 등록했다. 🚨onConfirm = App.jsx에서 handleRemovePlace 함수)handleRemovePlace를 실행할 때마다 isModalOpen 상태를 업데이트 하고 해당 상태에 따라서 Modal이 열림/닫힘을 결정한다DeleteConfirmation 컴포넌트가 실행/실행되지 않음을 조건을 통해 결정했기 때문에 이 프로젝트는 무한 루프에 빠지지 않으나, 함수를 이용해 의존성을 파악하는 것은 좋은 방법이 아니다.useCallbackhandleRemovePlace 함수를 useCallback 훅 안에 넣어주면 된다.useCallback( 함수, 의존성 배열 ) → 주변 컴포넌트 함수가 다시 실행되는 경우마다 재생성되지 않게 한다.
useCallback을 사용하면useCallback안의 함수가 재생성되지 않도록 한다. 그 대신, 메모리로서 내부에 저장한다. 따라서 해당 컴포넌트가 재실행될 때마다 메모리로서 저장된 함수를 재사용한다.
// App.jsx
const handleRemovePlace = useCallback(function handleRemovePlace() {
setPickedPlaces((prevPickedPlaces) =>
prevPickedPlaces.filter((place) => place.id !== selectedPlace.current)
);
setModalIsOpen(false);
const storedIds = JSON.parse(localStorage.getItem("selectedPlaces")) || [];
localStorage.setItem(
"selectedPlaces",
JSON.stringify(storedIds.filter((id) => id !== selectedPlace.current))
);
}, []);
useEffect의 의존성으로 함수를 사용할 경우,useCallback을 사용한다.
useEffect의 종속성 배열이 빈 것과 같은 의미이다.useEffect와 같은 의미이므로 prop이나 state값을 전달하면 된다.useEffect의 Cleanup 함수 : 다른 예시import { useEffect, useState } from "react";
const TIMER = 3000;
export default function DeleteConfirmation({ onConfirm, onCancel }) {
const [remainingTime, setRemainingTime] = useState(TIMER);
useEffect(() => {
const interval = setInterval(() => {
console.log("INTERVAL");
setRemainingTime((prevTime) => prevTime - 10); // 매 10밀리초 마다 계속 State를 업데이트 -> DeleteConfirmation 컴포넌트 업데이트
}, 10);
return () => {
clearInterval(interval);
};
}, []); // 의존성 없음. DeleteConfirmation 컴포넌트가 삭제될 때 같이 삭제.
useEffect(() => {
const timer = setTimeout(() => {
console.log("TIMER SET");
onConfirm();
}, 3000);
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>
<progress value={remainingTime} max={TIMER} />
</div>
);
}

import { useState, useEffect } from "react";
export default function ProgressBar({timer}) {
const [remainingTime, setRemainingTime] = useState(timer);
useEffect(() => {
const interval = setInterval(() => {
console.log("INTERVAL");
setRemainingTime((prevTime) => prevTime - 10);
}, 10);
return () => {
clearInterval(interval);
};
}, []); // 의존성 없음. DeleteConfirmation 컴포넌트가 삭제될 때 같이 삭제.
return <progress value={remainingTime} max={timer} />;
}
import { useEffect } from "react";
import ProgressBar from "./ProgressBar.jsx";
const TIMER = 3000;
export default function DeleteConfirmation({ onConfirm, onCancel }) {
useEffect(() => {
const timer = setTimeout(() => {
console.log("TIMER SET");
onConfirm();
}, 3000);
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>
<ProgressBar timer={TIMER} />
</div>
);
}