본문 바로가기

[Network] Effect Hook & Ajax 요청

[Network] Effect Hook & Ajax 요청
*   Effect Hook과 Ajax의 개념 및 활용
-  side effect로부터 분리하여 React 컴포넌트를 만들기 (비즈니스 로직과 표현 영역 구분)
-  Effect Hook과 Ajax를 사용해 서버로부터 데이터를 받아오는 방법
-  컴포넌트 내에서 네트워크 요청 시, 로딩 화면과 같이 보다 나은 UI 만들기
-  Effect Hook을 이용해 비동기 호출 및 AJAX 요청과 같은 side effect를 React 컴포넌트 내에서 처리하는 방법
    •  

 

 


 

Side Effect (부수 효과)

 

함수 내에서 어떤 구현이 함수 외부에 영향을 끼치는 경우 해당 함수는 Side Effect가 있다고 이야기한다.

  • React에서는 Side Effect가 발생했다고 말하는 경우는
    • 컴포넌트 내에서 fetch를 사용해 API 정보를 가져오거나
    • 이벤트를 활용해 DOM 직접 조작할 때이다.
  • 함수 내부에서 외부의 값에 관여하는 경우 Side Effect가 있다고 한다.

순수 함수의 출력값에 영향을 미치는 작업들이 바로 Side Effect라고 할 수 있겠다.

  • Side Effect를 최소화하거나 따로 분리하여 함수로 묶어주는 작업은 해당 프로젝트나 소프트웨어의 유지보수를 좀 더 수월하게 해준다.
다음은, bar라는 함수가 전역 변수 foo를 수정하는 예시이다.
let foo = 'hello';

function bar() {
  foo = 'world';
}

bar(); // bar는 Side Effect를 발생시킨다.

 

Pure Function (순수 함수)

 

순수 함수란, 오직 함수의 입력(매개변수)만이 함수의 결과에 영향을 주는 함수를 의미한다.

  • 함수의 입력이 아닌 다른 값이 함수의 결과에 영향을 미치는 경우, 순수 함수라고 부를 수 없다.
  • 또한 순수 함수는, 입력으로 전달된 값을 수정하지 않는다.
  • 순수 함수에는 네트워크 요청과 같은 Side Effect가 없다. 
  • 순수 함수의 특징 중 하나는, 어떠한 전달 인자가 주어질 경우, 항상 똑같은 값이 리턴됨을 보장한다.
    ->
    그래서 예측 가능한 함수이기도 하다.
function upper(str) {
  return str.toUpperCase(); // toUpperCase 메소드는 원본을 수정하지 않는다 (Immutable)
}

upper('hello') // 'HELLO'

 

Case Study

  • Math.random()은 순수 함수가 아니다. WHY?  
    • 순수 함수는 오직 함수의 입력(매개변수)만이 함수의 결과에 영향을 주는 함수이다.
      -> 순수 함수는 항상 예측 가능한 동일한 리턴값을 출력한다.
    • Math.ramdom() 같은 메서드는 동일한 입력에도 다른 출력이 나올 수 있으므로 순수 함수가 아니다.
  • Math.sqrt(x)는 순수 함수다. WHY?
    • Math.sqrt(x)는 전달 인자 x의 제곱근 값을 구하는 메서드이다.
    • 주어진 인자에 대해 항상 동일한 결과값을 리턴하므로 순수 함수이다.
  • fetch API를 이용해 AJAX 요청을 하는 함수는 순수 함수가 아니다. WHY?  
    • 어떤 함수가 서버에 네트워크 요청을 보낸다면 이로 인해 서버의 데이터에 Side Effect를 일으킬 수 있다.
    • 이에 따라 해당 함수는 순수 함수가 아니다.

 


 

React의 함수 컴포넌트

 

아래의 React의 함수 컴포넌트는, props가 입력으로, JSX Element가 출력으로 나간다.

  • 여기에는 그 어떤 Side Effect도 없으며, 순수 함수로 작동한다.
function SingleTweet({ writer, body, createdAt }) {
  return <div>
    <div>{writer}</div>
    <div>{createdAt}</div>
    <div>{body}</div>
  </div>
}

 

BUT!!!

하지만 보통 React 애플리케이션을 작성할 때에는,

  • AJAX 요청이 필요하거나, LocalStorage 또는 타이머와 같은 React와 상관없는 API를 사용하는 경우가 발생할 수 있다.
    • 이는 React의 입장에서는 전부 Side Effect 이다.
  • React는 Side Effect를 다루기 위한 HookEffect Hook을 제공한다.

 

(정리) React 컴포넌트에서의 Side Effect

  • 타이머 사용 (setTimeout)
  • 데이터 가져오기 (fetch API, localStorage)

 


 

Effect Hook

 

여기 명언을 보여주는 간단한 애플리케이션이 있다. 먼저 이 링크를 열어서 버튼을 클릭할 때마다 브라우저 상단의 타이틀이 어떻게 변경되는지 확인해 보자.

 

  • useEffect는 컴포넌트 내에서 Side effect를 실행할 수 있게 하는 Hook이다.
  • 이 컴포넌트에서 실행하는 Side effect는 브라우저 API를 이용하여, 타이틀을 변경하는 것이다. 

 


 

API

useEffect(함수, [종속성1, 종속성2, ...])

 

1) 첫 번째 인자      //     useEffect(함수, [종속성1, 종속성2, ...])

  • useEffect의 첫 번째 인자는 함수이다. 해당 함수 내에서 side effect를 실행하면 된다.
  • 이 함수는 다음과 같은 조건에서 실행된다.
  • 컴포넌트 생성 후 처음 화면에 렌더링(표시)
  • 컴포넌트에 새로운 props가 전달되며 렌더링
  • 컴포넌트에 상태(state)가 바뀌며 렌더링

이와 같이 매번 새롭게 컴포넌트가 렌더링 될 때 Effect Hook이 실행된다.

 

2) 두 번째 인자  -  조건부 effect 발생 (dependency array)   //     useEffect(함수, [종속성1, 종속성2, ...])

  • useEffect의 두 번째 인자는 배열이다. 이 배열은 Effect hook의 조건을 담고 있다.  
  • 여기서 조건은 boolean 형태의 표현식이 아닌, 배열 내의 어떤 값의 변경이 일어날 때를 의미한다.
  • 따라서, 해당 배열엔 어떤 값의 목록이 들어간다.
  • 이 배열을 특별히 종속성 배열이라고 부른다. 
  • 배열 내의 종속성1 또는 종속성2의 값이 변할 때, effect가 발생하는 첫 번째 인자의 함수가 실행된다.

 

[정리]

useEffect 의 두 번째 인자는 effect hook의 첫 번째 인자인 함수가 실행되는 조건을 담고 있다.

 

 

이 예제에서 개발자 콘솔의 값을 확인해보자. 여기에는 다음과 같은 세 상태가 존재한다.

  • 명언 목록 (proverbs)
  • 필터링할 문자열 (effect)
  • 카운트 (count)

 

이 예제는, filter가 변할 때에만, effect 함수가 실행된다. (개발자 콘솔을 통해 확인할 수 있다.)

한편, 카운트를 올리는 버튼은 컴포넌트의 상태가 바뀌고 업데이트되지만, 아무리 버튼을 눌러도 effect 함수는 실행되지 않는다.

  • 왜냐하면, 종속성 배열에는 filter만 존재하고, count는 존재하지 않기 때문이다.

 


 

단 한 번만 실행되는 Effect 함수

 

(1) useEffect(함수, [])

  • 빈 배열 넣기 (종속성 목록에 아무런 종속성도 없다)
  • 빈 배열을 useEffect의 두 번째 인자로 사용하면, 이때는 컴포넌트가 처음 생성될 때만 effect 함수가 실행된다.
  • 대표적으로 처음 단 한 번, 외부 API를 통해 리소스를 받아오고 더 이상 API 호출이 필요하지 않을 때에 사용할 수 있다.
useEffect(() => {
	console.log(몇 번 호출될까요?)
},[])

// 컴포넌트가 처음 생성될 때만 함수가 실행
// Effect Hook을 컴포넌트 내에서 최초 1회만 호출하고 싶을 때 알맞음

 

 

(2) useEffect(함수)

  • 아무것도 넣지 않기 (기본 형태)
  • 기본 형태의 useEffect 는 컴포넌트가 처음 생성되거나, props가 업데이트되거나, 상태(state)가 업데이트될 때 effect 함수가 실행된다.
useEffect(() => {
	console.log(몇 번 호출될까?)
})

// 컴포넌트가 처음 생성되거나, props가 업데이트 되거나, state가 업데이트 될 때마다 실행

 

 

(3) useEffect(함수, [dep])

  • dep이 업데이트 될 때마다 실행된다.
useEffect(() => {
	console.log(몇 번 호출될까요?)
},[dep])

// dep이 업데이트 될 때마다 실행

 

 
코드로 정리
function App() {
	const [keyword, setKeyword] = userState("");
	const onChange = (event) => seyKeyword(event.target.value);

// 아무것도 안 들어가 있을 때는 컴포넌트가 처음 생성되거나, props가 업데이트 되거나, state가 업데이트 될 때마다 실행
	useEffect(() => {
		console.log("나는 업데이트가 될 때마다 작동해")
	})
    
// 빈 배열일 때는 컴포넌트가 처음 생성될 때만 함수가 실행
	useEffect(() => {
		console.log("나는 처음만 작동해")
	}, [])

// keyword가 업데이트 될 때마다 실행
	useEffect(() => {
		console.log("나는 keyword가 바뀔 때마다 작동해")
	}, [keyword])

	return (
		<div>
			<input
				value={keyword}
				onChange={onChange}
				type="text"
				placeholder="검색창..."
			/>
		</div>
	)
}
 

 

Hook을 쓸 때 주의할 점 (공식 문서)

  • 최상위에서만 Hook을 호출한다.
  • React 함수 내에서 Hook을 호출한다.
    • 즉, 반복문과 조건부 내부에서 사용하지 않는다.
      -> React Hook은 최상위에서만 호출해야 한다.
      -> 그래야만 컴포넌트가 렌더링 될 때마다 항상 동일한 순서의 Hook 호출이 보장된다.
      -> 만약 반복문 또는 조건문 내부에서 Hook을 사용하게 되면, React가 올바르게 각
      Hook의 상태를 유지할 수 없게 된다.
           (자세한 설명은 리액트 공식 문서 Hook의 규칙 하단 설명 란을 참고)

 


 

Data Fetching : 목록 내 필터링 구현하기 예제

 

목록 내 필터링을 구현하기 위해서는 다음과 같은 두 가지 접근이 있을 수 있다.

  1. 컴포넌트 내에서 필터링:
    • 전체 목록 데이터를 불러오고, 목록을 검색어로 filter 하는 방법
  2. 컴포넌트 외부에서 필터링:
    • 컴포넌트 외부로 API 요청을 할 때, 필터링 한 결과를 받아오는 방법
    • (보통, 서버에 매번 검색어와 함께 요청하는 경우가 이에 해당)

 

1. 컴포넌트 내에서 필터링

처음 단 한 번, 외부 API로부터 명언 목록을 받아오고, filter 함수를 이용한다.

// storageUtil.js : API 대신 구현

localStorage.setItem(
  "proverbs",
  JSON.stringify([
    "좌절감으로 배움을 늦추지 마라",
    "Stay hungry, Stay foolish",
    "Memento Mori",
    "Carpe diem",
    "배움에는 끝이 없다"
  ])
);

export function getProverbs(filterBy = "") {
  const json = localStorage.getItem("proverbs");
  const proverbs = JSON.parse(json) || [];
  return proverbs.filter((prvb) =>
    prvb.toLowerCase().includes(filterBy.toLowerCase())
  );
}
import { useEffect, useState } from "react";
import "./styles.css";
import { getProverbs } from "./storageUtil";   // 속담을 받아온다.

export default function App() {
  const [proverbs, setProverbs] = useState([]);    // proverbs state hook
  const [filter, setFilter] = useState("");        // filter state hook

  useEffect(() => {    
    console.log("언제 effect 함수가 불릴까?");         // effect 함수는 처음 한 번 실행된다.
    const result = getProverbs();
    setProverbs(result);                           // proverb 상태가 result 값으로 변경
  }, []);

  const handleChange = (e) => {
    setFilter(e.target.value);
  };

  return (
    <div className="App">
      필터
      <input type="text" value={filter} onChange={handleChange} />
      <ul>
        {proverbs
          .filter((prvb) => {                          // 🟣 클라이언트에서 필터 
            return prvb.toLowerCase().includes(filter.toLowerCase());  
          })                                           // 🟣 소문자로 통일 후 필터 
          .map((prvb, i) => (
            <Proverb saying={prvb} key={i} />
          ))}
      </ul>
    </div>
  );
}
// 아래 속담 목록 출력하는 컴포넌트 (필터를 걸면 해당 속담만 다시 렌더링)
function Proverb({ saying }) {            
  return <li>{saying}</li>;
}

 

2. 컴포넌트 외부에서 필터링

검색어가 바뀔 때마다, 외부 API를 호출한다. 

import { useEffect, useState } from "react";
import "./styles.css";
import { getProverbs } from "./storageUtil";

export default function App() {
  const [proverbs, setProverbs] = useState([]);
  const [filter, setFilter] = useState("");
  const [count, setCount] = useState(0);       // 카운터 기능도 추가해봄

  useEffect(() => {
    console.log("언제 effect 함수가 불릴까?");     // 필터 입력이 될 때마다 불린다.  
    const result = getProverbs(filter);        // filter 값이 포함된 속담만 서버에서 가져온다.
    setProverbs(result);
  }, [filter]);                                // 종속성은 filter의 값으로 
                                               //해당 값이 변화할 때마다 effect 함수가 실행된다.
  const handleChange = (e) => {
    setFilter(e.target.value);
  };

  const handleCounterClick = () => {
    setCount(count + 1);
  };

  return (
    <div className="App">
      필터
      <input type="text" value={filter} onChange={handleChange} />
      <ul>                           
        {proverbs.map((prvb, i) => (                // 🟣 map만 있다. 
          <Proverb saying={prvb} key={i} />
        ))}
      </ul>
      <button onClick={handleCounterClick}>카운터 값: {count}</button>
    </div>
  );
}
function Proverb({ saying }) {
  return <li>{saying}</li>;
}

 

두 방식의 차이점

각각의 장단점은 무엇일까?

  • 지금은 storageUtil.js를 이용해 외부 API를 직접 구현했지만(LocalStorage API를 이용), 이는 서버 요청으로 대체할 수 있다.
  • 만일 서버에서 수십만 개의 명언을 제공한다고 가정해 보자.
  • 다음의 표는 HTTP를 이용한 서버 요청을 가정할 때, 두 방식의 차이점을 설명하고 있다.
  장점 단점
컴포넌트 내부에서 처리 HTTP 요청의 빈도를 줄일 수 있다 브라우저(클라이언트)의 메모리 상에 많은 데이터를 갖게 되므로, 클라이언트의 부담이 늘어난다
컴포넌트 외부에서 처리 클라이언트가 필터링 구현을 생각하지 않아도 된다 빈번한 HTTP 요청이 일어나게 되며, 서버가 필터링을 처리하므로 서버가 부담을 가져간다

 

AJAX 요청을 보내자!

웹 앱을 구성할 때, 서버로의 네트워크 요청을 보내야 되는 경우가 있다.

  • React에서는 이러한 Ajax 요청을 처리할 때, Side Effect를 최소화하기 위해서 Effect Hook을 사용한다.
  • 만약 훅을 사용하지 않고 네트워크 요청을 하면 그 동안에 페이지가 멈추거나 깜빡일 수 있다.

임의로 구현한 storageUtil.js 대신, fetch API를 써서, 서버에 요청한다면 코드는 어떻게 될까?

  • 명언을 제공하는 API의 엔드포인트가 http://서버주소/proverbs 라고 가정해 보자. 
useEffect(() => {
  fetch(`http://서버주소/proverbs?q=${filter}`)
    .then(resp => resp.json())
    .then(result => {
      setProverbs(result);
    });
}, [filter]);

 

AJAX 요청이 매우 느릴 경우?

모든 네트워크 요청이 항상 즉각적인 응답을 가져다주는 것은 아니다.

외부 API 접속이 느릴 경우를 고려하여, 로딩 화면(loading indicator)의 구현은 필수적이다.

[그림] loading indicator

 

 

[그림] loading placeholder

 

 

기본적으로, Loading indicator의 구현은 어떻게 처리할 수 있을까? 여기에도 상태 처리가 필요하다.

const [isLoading, setIsLoading] = useState(true);

// 생략, LoadingIndicator 컴포넌트는 별도로 구현했음을 가정 
return {isLoading ? <LoadingIndicator /> : <div>로딩 완료 화면</div>}

 

fetch 요청의 전후로 setIsLoading을 설정해 주어 보다 나은 UX를 구현할 수 있다.

useEffect(() => {
  setIsLoading(true);
  fetch(`http://서버주소/proverbs?q=${filter}`)
    .then(resp => resp.json())
    .then(result => {
      setProverbs(result);
      setIsLoading(false);
    });
}, [filter]);

 

아래 Main 컴포넌트의 첫 화면은 로딩 화면은 따로 없이 항공편 리스트를 보여준다.
*  항공편 검색 시에 로딩 상태를 보여주고, 검색이 완료되면 그에 맞는 항공편 리스트가 보이도록 구현
import LoadingIndicator from './component/LoadingIndicator'

export default function Main() {
  const [isLoading, setIsLoading] = useState(false)  // loading useState 세팅
	//생략, condition State와 flightList State는 구현했음을 가정 

  useEffect(() => {
    setIsLoading(true)                               // 갱신함수를 실행 
		fetch(`http://서버주소/flight?q=${condition}`)
    .then(resp => resp.json())
    .then(result => {
			setFlightList(result)
            setIsLoading(false)
    });
  }, [condition]) 

//생략, search 컴포넌트와 flightList 컴포넌트는 구현했음을 가정 
  return (
    <div>
      <main>
        <div className="table">
          {isLoading ? <LoadingIndicator /> : <div>로딩 완료 화면</div>}  
        </div>
      </main>
    </div>
  )
}
728x90

'FE > Network' 카테고리의 다른 글

(비)[Network] HTTP모듈을 Express로 리팩토링  (0) 2023.04.05
[Network] SOP & CORS  (0) 2023.04.04
[Network] Postman으로 HTTP 요청 및 응답 받아오기  (0) 2023.03.30
[Network] Open API란?  (0) 2023.03.30
[Network] REST API  (0) 2023.03.29
⬆︎