앞에서는 React 컴포넌트 내부의 데이터 흐름에 대해서 공부했다.
지금부터는 React 컴포넌트 외부에서 데이터를 처리하고 받아오는 과정을 공부해보자.
Side Effect (부수 효과)
함수 내에서 어떤 구현이 함수 외부에 영향을 끼치는 경우 해당 함수는 Side Effect가 있다고 한다.
React에서는 컴포넌트 내에서 fetch를 사용해 API 정보를 가져오거나
이벤트를 활용해 DOM 직접 조작할 때 Side Effect가 발생했다고 말합니다.
let foo = 'hello';
function bar() {
foo = 'world';
}
bar(); // bar는 Side Effect를 발생시킵니다!
Pure Function (순수 함수)
순수 함수란, 오직 함수의 입력만이 함수의 결과에 영향을 주는 함수를 의미한다.
함수의 입력이 아닌 다른 값이 함수의 결과에 영향을 미치는 경우, 순수 함수라고 부를 수 없다.
또한 순수 함수는, 입력으로 전달된 값을 수정하지 않다.
function upper(str) {
return str.toUpperCase(); // toUpperCase 메소드는 원본을 수정하지 않습니다 (Immutable)
}
upper('hello') // 'HELLO'
순수 함수에는 네트워크 요청과 같은 Side Effect가 없다.
순수 함수의 특징 중 하나는, 어떠한 전달 인자가 주어질 경우, 항상 똑같은 값이 리턴됨을 보장한다.
그래서 예측 가능한 함수이기도 하다.
💡 Math.random()은 순수 함수가 아니다. 왜일까?
➡️ 예측이 불가능하기 때문에
💡 어떤 함수가 fetch API를 이용해 AJAX 요청을 한다고 가정해 보자. 이 함수는 순수 함수가 아니다. 왜일까?
➡️ Ajax 요청은 외부 상태를 바꾸기 때문에
React의 함수 컴포넌트
우리가 앞서 배운 React의 함수 컴포넌트는, props가 입력으로, JSX Element가 출력으로 나간다.
여기에는 그 어떤 Side Effect도 없으며, 순수 함수로 작동한다.
function SingleTweet({ writer, body, createdAt }) {
return <div>
<div>{writer}</div>
<div>{createdAt}</div>
<div>{body}</div>
</div>
}
하지만 보통 React 애플리케이션을 작성할 때에는, AJAX 요청이 필요하거나,
LocalStorage 또는 타이머와 같은 React와 상관없는 API를 사용하는 경우가 발생할 수 있다.
이는 React의 입장에서는 전부 Side Effect이다.
React는 Side Effect를 다루기 위한 Hook인 Effect Hook을 제공한다.
useEffect
는 컴포넌트 내에서 Side effect를 실행할 수 있게 하는 Hook이다.
이 예제에서 실행하는 Side effect는 브라우저 API를 이용하여, 타이틀을 변경하는 것이다.
💡 API
useEffect(함수)
useEffect의 첫 번째 인자는 함수이다. 해당 함수 내에서 side effect를 실행하면 된다.
그럼 어떤 조건에서 언제 실행될까?
이와 같이 매번 새롭게 컴포넌트가 렌더링 될 때 Effect Hook이 실행된다.
주의할 점으로는 최상위와 React 함수 내에서만 Hook 호출해야 한다.
useEffect의 두 번째 인자는 배열이다. 이 배열은 조건을 담고 있다.
여기서 조건은 boolean 형태의 표현식이 아닌, 어떤 값의 변경이 일어날 때를 의미한다.
따라서, 해당 배열엔 어떤 값의 목록이 들어간다. 이 배열을 특별히 종속성 배열이라고 부른다.
이 예제를 열어보고, 개발자 콘솔의 값을 확인할 수 있다. 여기에는 다음과 같은 세 상태가 존재한다.
이 예제는, filter가 변할 때에만, effect 함수가 실행된다.
한편, 카운트를 올리는 버튼은 컴포넌트의 상태가 바뀌고 업데이트되지만, 아무리 버튼을 눌러도 effect 함수는 실행되지 않는다. 왜냐하면, 종속성 배열에는 filter만 존재하고, count는 존재하지 않기 때문이다.
💡 그럼 카운트 버튼을 눌렀을 때에도 effect 함수를 실행시키려면 어떻게 해야 될까?
useEffect(함수, [종속성1, 종속성2, ...])
useEffect의 두 번째 인자는 종속성 배열이다.
배열 내의 종속성1, 또는 종속성2의 값이 변할 때, 첫 번째 인자의 함수가 실행된다.
단 한 번만 실행되는 Effect 함수
만일 종속성 목록에 아무런 종속성도 없다면 어떤 일이 발생할까?
즉, 두 번째 배열을 빈 배열[]로 둘 경우에는 무슨 일이 발생할까?
두 번째 인자를 아예 안 넘기는 것과 어떻게 다를까?
1. 빈 배열 넣기
useEffect(함수, [])
2. 아무것도 넣지 않기 (기본 형태)
useEffect(함수)
(2번) 기본 형태의 useEffect는 컴포넌트가 처음 생성되거나,
props가 업데이트되거나, 상태(state)가 업데이트될 때 effect 함수가 실행됨을 앞서 배웠다.
반면에 (1번) 빈 배열을 useEffect의 두 번째 인자로 사용하면, 컴포넌트가 처음 생성될 때만 effect 함수가 실행된다.
💡 이것이 언제 필요할까?
대표적으로 처음 단 한 번, 외부 API를 통해 리소스를 받아오고 더 이상 API 호출이 필요하지 않을 때에 사용할 수 있다.
이번 과제는 어제 사이트를 더 업그레이드하는 것이었다.
기존 코드는, 컴포넌트 내부 자체에서 데이터를 filter하는 방식이었다.
오늘은 이 코드를 컴포넌트 외부에서 api를 받아온 후, filter하여 Main 컴포넌트로 넘겨주는 방식으로 업데이트 한다.
둘은 컴포넌트의 내부 외부, 위치에 따라 작업을 처리는 차이점이 있으며 각각 장단점은 다음과 같다.
1. 외부 컴포넌트에 검색 값 넘겨주기 (Side effect)
useEffect(() => {
const fetchData = async () => {
const data = await getFlight(condition); // getFlight 함수를 호출하여 항공편 목록을 가져온다.
setFlightList(data); // 가져온 항공편 목록을 상태에 저장한다.
};
fetchData();
}, [condition]);
상태 갱신된 condition을 useEffect를 통해 getFlight로 넘겨준다.
useEffect로 인해, condition이 변경 될때마다 넘겨준다.
2. fetch로 API 받아오기
// 기존 코드 - 내부에서 처리
import flightList from "../resource/flightList";
import fetch from "node-fetch";
if (typeof window !== "undefined") {
localStorage.setItem("flight", JSON.stringify(flightList));
}
export function getFlight(filterBy = {}) {
// HINT: 가장 마지막 테스트를 통과하기 위해, fetch를 이용합니다. 아래 구현은 완전히 삭제되어도 상관없습니다.
// TODO: 아래 구현을 REST API 호출로 대체하세요.
let json = [];
if (typeof window !== "undefined") {
json = localStorage.getItem("flight");
}
const flight = JSON.parse(json) || [];
return new Promise((resolve) => {
const filtered = flight.filter((flight) => {
let condition = true;
if (filterBy.departure) {
condition = condition && flight.departure === filterBy.departure;
}
if (filterBy.destination) {
condition = condition && flight.destination === filterBy.destination;
}
return condition;
});
setTimeout(() => {
resolve(filtered);
}, 500);
});
}
// 수정한 코드 - 외부에서 처리
import flightList from "../resource/flightList";
import fetch from "node-fetch";
if (typeof window !== "undefined") {
localStorage.setItem("flight", JSON.stringify(flightList));
}
export function getFlight(filterBy = {}) {
const queryParams = [];
// filterBy.departure가 존재하는 경우
if (filterBy.departure) {
// queryParams 배열에 departure 필터링 조건을 추가
queryParams.push(`departure=${filterBy.departure}`);
}
// filterBy.destination이 존재하는 경우
if (filterBy.destination) {
// queryParams 배열에 destination 필터링 조건을 추가
queryParams.push(`destination=${filterBy.destination}`);
}
// queryParams 배열을 &로 연결하여 쿼리 스트링 생성
const queryString = queryParams.length > 0 ? `?${queryParams.join("&")}` : "";
// API 엔드포인트에 쿼리 스트링을 포함하여 fetch 요청을 보냄
return fetch(
`http://ec2-43-201-32-255.ap-northeast-2.compute.amazonaws.com/flight${queryString}`
)
.then((response) => response.json()) // 응답 데이터를 JSON으로 변환
.then((data) => data); // 변환된 데이터 반환
}
파라미터는 다음 조건에 따라 queryString 변수를 만들어 수정하였다.
처음에는 익숙하지 않았는데, fetch | .json() | data 는 한 세트라고 생각하면 되겠다.
이제 마지막으로 API를 가져오는 동안, 로딩바를 만들어줄 것이다.
파일 안에는 이미 로딩 컴포넌트가 구현되어 있어서 갔다가 쓰기만 하면 된다.
3. 로딩 화면 제공
const [isLoading, setIsLoading] = useState(false);
...
useEffect(() => {
// 1. Async/Await
const fetchData = async () => {
setIsLoading(true); // 로딩 여부를 참으로 만들고
const data = await getFlight(condition); // getFlight 함수를 호출하여 항공편 목록을 가져옵니다.
setFlightList(data); // 가져온 항공편 목록을 상태에 저장합니다.
setIsLoading(false); // 로딩 여부를 거짓으로 만들기
};
fetchData();
}, [condition]);
...
{isLoading ? <LoadingIndicator /> : <FlightList list={flightList} />}
먼저 로딩은 boolean 값으로 구분할 수 있으므로, state인 useState 함수로 지정해주었다.
그다음 LoadingIndicator 로딩 컴포넌트를 가져와서 항공편 리스트가 로딩 중이라면, 로딩 컴포넌트를 보여주고
로딩이 끝났다면, 가져온 항공편 리스트를 보여주도록 하기 위해 삼항연산자로 묶어주었다.
당연히 여기서 끝이 아니다. 이는 state이므로 당연히 상태 변화를 지정해주어야 한다.
useEffect 안에 async 로 인해 순차적으로 진행되게끔 유도해주었다.
처음에는 로딩을 참으로 바꿨고, 항공편 목록을 가져오고 저장한 후, 로딩을 거짓으로 바꿔서 로딩바가 보이지 않도록 했다.