Front End/React

클라이언트 Ajax 요청

상태 끌어올리기 (Lift State Up)

React 데이터 흐름

앞서 배웠듯 React 개발은 컴포넌트부터 개발을 시작해 하나로 모아가는 방식으로 상향식(bottom-up) 개발이고, 데이터는 같은 데이터를 사용하는 컴포넌트의 상위 컴포넌트에 데이터를 배치하는 하향식(top-down) 흐름을 가진다. 하지만 상위 컴포넌트에 배치된 상태를 하위 컴포넌트에서 변경해야 하는 경우도 이따금씩 발생한다.

단방향 데이터 흐름이라는 원칙에 따라 하위 컴포넌트는 상위 컴포넌트로부터 전달받은 데이터의 형태나 타입이 무엇인지만 알 수 있고, 데이터가 state에서 왔는지 하드코딩으로 입력했는지는 알 수 없다. 그래서 하위 컴포넌트에서 특정 이벤트로 인해 상위 컴포넌트의 상태가 바뀌는 것은 ‘역방향 데이터 흐름’ 처럼 기존에 우리가 알던 단방향과는 다른 이야기로 들린다.

단방향 데이터 흐름을 해치지 않는 React의 해결법

그래서 React는 “상위 컴포넌트의 상태를 변경하는 함수 자체를 하위 컴포넌트로 전달하여 하위 컴포넌트가 실행하게 한다”는 해결책을 제시함으로서 (함수일지라도)데이터를 하위로 전달한다는 단방향 데이터 흐름의 원칙에 부합하게끔 했다. 이를 상태 끌어올리기(Lift State Up)이라 한다.

간단한 방법 : 상태 변경 함수를 props로 전달

import React, { useState } from "react";

export default function ParentComponent() {
  const [value, setValue] = useState("날 바꿔줘!");

  const handleChangeValue = () => {
    setValue("보여줄게 완전히 달라진 값");
  };

  return (
    <div>
      <div>값은 {value} 입니다</div>
      <ChildComponent />
    </div>
  );
}

function ChildComponent() {
  const handleClick = () => {
    
  };

  return <button onClick={handleClick}>값 변경</button>;
}

예제의 value는 상위 컴포넌트의 상태로, setValue라는 상태 변경 함수를 사용할 수 있다. 하위 컴포넌트에서 value의 상태를 변경하려면, props로 setValue를 전달하면 된다.

function ParentComponent() {
  const [value, setValue] = useState("날 바꿔줘!");

  const handleChangeValue = (newValue) => {
    setValue(newValue);
  };

  return (
    <div>
      <div>값은 {value} 입니다</div>
			{// props로 함수 자체를 전달}
      <ChildComponent handleButtonClick={handleChangeValue}  />
    </div>
  );
}


function ChildComponent({ handleButtonClick }) {
  const handleClick = () => {
		// 구조 분해 할당 문법으로 props 그대로 활용, 함수 실행 역시 가능
    handleButtonClick('자식이 원하는 값');
  }

  return (
    <button onClick={handleClick}>값 변경</button>
  )
}

handleButtonClick에 상태를 변경하는 함수handleChangeValue를 그대로 할당했기 때문에, 하위 컴포넌트에서는 전달받은 props를 그대로 함수처럼 사용하면 상위 컴포넌트의 상태 변경이 가능하다. 이 때, 필요하다면 설정할 값을 콜백함수의 인자로도 넘길 수 있다.

Effect Hook

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'
  • Math.random()은 입력값이 영향을 주는 것과 별개로 예측 불가능한 랜덤한 값을 리턴하기 때문에 순수 함수가 될 수 없다.
  • 어떤 함수가 fetch API를 이용해 AJAX 요청을 한다면 이는 서버의 데이터에 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>
}

하지만 보통 React 어플리케이션을 작성할 때 AJAX 요청이 필요하거나, LocalStorage 또는 타이머와 같이 React와 상관 없는 API를 사용하는 경우도 발생한다. 이는 React에서 전부 Side Effect이며, React는 Side Effect를 다루기 위한 Hook인 Effect Hook을 제공한다.

React 컴포넌트에서의 Side Effect

React에서의 Side Effect는 컴포넌트가 화면에 렌더링된 이후에 비동기로 처리되어야 하는 부수적인 효과들을 말한다. 사용자 경험 관점에서, 만약 API가 응답이 늦어지거나 응답이 없는 상황에서 데이터를 받아오기 전 화면을 먼저 렌더링을 하고 그 이후에 비동기로 데이터를 가져오는 것이 더 나은 경험을 제공하기 때문에 실제 데이터는 비동기로 가져오는 것이 권장된다.

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

Effect Hook(useEffect)

useEffect()는 컴포넌트 내에서 Side Effect를 실행할 수 있게 하는 Hook이다. 컴포넌트는 순수 함수로 작동되어야 하기 때문에, 이에 위배되는 기능들을 Effect Hook을 통해 처리한다. 해당 Hook을 사용하면 컴포넌트가 렌더링 된 이후에 실행시킬 함수나 API를 사전에 정하여 개발자가 원하는대로 실행되게끔 만들 수 있다.

useEffect(Function, [data1, data2, …])
이 컴포넌트에서 실행하는 Side effect는 버튼 아래의 텍스트만 변경하는 것이 아닌 브라우저의 타이틀을 변경하는 것이다. 브라우저 API를 이용했다.

첫 번째 전달인자는 함수이며, 이 함수는 컴포넌트 생성이나 컴포넌트에 새로운 props가 전달되거나 컴포넌트에 상태가 바뀌는 등 매번 새롭게 컴포넌트가 렌더링 될 때 Effect Hook이 실행된다.

조건부 effect 발생

useEffect의 두 번째 인자는 배열이며, 이 배열은 조건을 담고 있다. 여기서 조건은 boolean 형태의 표현식이 아닌, 어떤 값의 변경이 일어날 때를 의미한다. 따라서 해당 배열에는 어떤 값의 목록이 들어가며, 이 배열을 특별히 종속성 배열(dependency array)이라 부른다.

  • 종속성 배열의 위치에 빈 배열을 넣으면 컴포넌트가 처음 생성될 때만 실행된다. 대표적으로는 외부 API를 통해 리소스를 받아오고 더이상 API 호출이 필요하지 않을 때 사용한다.
  • 두 번째 인자를 사용하지 않으면 앞서 설명한 것 처럼 컴포넌트가 렌더링 될 때 마다 실행된다.

주의사항

  • 최상위에서만 Hook을 호출한다 → Hook의 실행은 호출되는 순서에 의존하기 때문에, 하위 컴포넌트 곳곳에 Hook을 사용하게 되면 렌더링 될 때마다 동일한 순서로 Hook이 호출되는 것을 보장받지 못한다.
  • React 함수 내에서 Hook을 호출한다 → 일반적인 JavaScript 함수에서 호출하지 않는다.

Data Fetching: 필터링 예제 살펴보기

서버에서 받아온 리소스를 검색어를 통해 필터링해야 하는 경우, 우리는 두 가지 접근 방식에 대해 생각해볼 수 있다. 두 방식은 접근 방식의 차이 일 뿐 모두 사용할 수 있으며 각각의 장단점에 따라 필요한 방식을 선택할 수 있다.

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

처음 단 한 번 외부 API로 부터 명언 목록을 받아오고, 목록을 검색어로 filter 하는 방법이다. 서버에 요청을 한 번 보내기 때문에 HTTP 요청의 빈도를 줄이고 서버의 부담 역시 줄어들지만 브라우저(클라이언트)의 메모리 상에 많은 데이터를 갖게 되어 클라이언트의 부담이 늘어난다.

// (생략)

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

  useEffect(() => {
    console.log("언제 effect 함수가 불릴까요?");
    fetch(`http://서버주소/proverbs`)
    .then(resp => resp.json())
    .then(result => {
      setProverbs(result);
    });
  }, []); // 빈 배열을 두 번째 인자로 줘서 처음 컴포넌트가 생성 될 때만 Hook 호출

// (생략)

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

검색어가 바뀔 때마다 외부 API를 호출하여 filter 하는 방법이다. 클라이언트가 필터링 구현을 생각하지 않아도 되어 간편하지만 빈번한 HTTP 요청이 일어나며 서버가 필터링 하므로 서버가 부담을 가져가게 된다.

// (생략)

export default function App() {
  const [proverbs, setProverbs] = useState([]);
  const [filter, setFilter] = useState("");
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("언제 effect 함수가 불릴까요?");
    fetch(`http://서버주소/proverbs?q=${filter}`) // 파라미터를 이용해 필터된 리소스 요청
    .then(resp => resp.json())
    .then(result => {
      setProverbs(result);
    });
  }, [filter]); // filter 상태가 변경될 때마다 Hook 호출

// (생략)

fetch를 이용한 AJAX 요청 보내기

이전에 배웠던 프로미스의 .then 메서드를 사용해 fetch API로 서버에 요청해 데이터를 가져올 수 있다. 이 때, fetch만 사용 했을 때 받는 응답은 Response 객체로, 단순한 JSON가 아닌 headers나 응답 상태 코드 등 다양한 정보를 담고 있어 바로 사용할 수가 없기 때문에 .json()을 사용해 사용 가능한 데이터로 파싱을 해주는 과정이 꼭 필요하다.

useEffect(() => {
  fetch(`http://서버주소/proverbs?q=${filter}`)
  .then(resp => resp.json()) // Response 객체를 .json()으로 파싱해서 받아올 데이터만 리턴
  .then(result => { // 리턴 받은 데이터를 상태에 새로 할당
    setProverbs(result);
  });
}, [filter]); // filter 상태가 변경될 때마다 Hook 호출

AJAX 요청이 매우 느릴 경우엔, 로딩화면

모든 네트워크 요청이 항상 즉각적인 응답을 가져다 주지 않기 때문에 외부 API 접속이 느릴 경우를 고려해 로딩 화면(loading indicator)는 필수적으로 구현해야 한다.

loading indicator
loading placeholder

기본적으로 로딩 화면은 상태(state)를 이용해 처리할 수 있다. fetch 요청의 전후로 상태를 변경하여 그에 맞게 로딩화면을 렌더했다가 사라지게 하면 나은 UX를 구현할 수 있다.

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

{isLoading ? <LoadingIndicator /> : <div>로딩 완료 화면</div>}

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

Uploaded by N2T