S2 Unit 9. React 클라이언트 Ajax 요청

나현·2022년 10월 11일
0

학습일지

목록 보기
26/53
post-thumbnail

💡 이번에 배운 내용

  • Section2.
    서버와 통신이 가능한 구조적인 Web App을 만들 수 있다.
  • Unit9. React 클라이언트 Ajax 요청: React 데이터 흐름에 대해 복습하고, Effect Hook과 Ajax를 사용해 서버로부터 데이터를 받아오는 방법에 대해서 학습한다.

느낀점

내용 정리에 상당한 시간이 걸렸지만 그만큼 정리하면서 공부가 되어 좋은 기회였다. 이번 내용도 실습해보니 상당히 쉽지 않다. 역시 네트워크가 관련된 실습은 (특히 비동기) 쉽지않다.
괜히 비동기 데이터를 받아올 때 함수 내에서 변수에 담아가며 동기적으로 작업하여 값을 리턴했다.
하지만 간단하게 바로 fetch와 then을 연결시킨 값을 리턴하니까 동기적으로 쓸 필요도 없었다...
비동기 작업시 리턴 시점도 참 중요하구나... 잊지말자🥲


키워드

역방향 데이터 흐름, state 끌어올리기, 부수 효과(side Effect), 순수 함수(pure Function), Effect Hook, React에서의 AJAX 요청, loading indicator, loading placeholder


학습내용

Ch1. React 데이터 흐름

이번 학습내용은 지난 번 학습했던 내용을 복습했고, 거기에 개념을 추가하는 방식이었다.
지난번 학습내용은 아래와 같다.

► 복습1: 리액트 설계시 몇가지 규칙과 팁

  • 단일 책임 원칙: 한 컴포넌트는 하나의 기능만 한다.
  • 상향식(bottom-up) 페이지 조립: 자식 컴포넌트부터 만들어서 부모 컴포넌트를 조립해 나간다.
  • 하향식(top-down) 데이터 흐름: 부모 컴포넌트로부터 props를 이용해 데이터를 마치 전달인자(arguments), 속성(attributes)처럼 전달받을 수 있다. 그리고 이 흐름은 한 방향이어야 한다.

► 복습2: state 특징

  • 부모로부터 props를 통해 전달되지 않는다. 해당 컴포넌트 상위에서 직접 사용한다.
  • 변하는 값이어야 한다.
  • 컴포넌트 안의 다른 state나 props를 가지고 계산할 수 없어야 한다.
  • 컴포넌트들이 공통된 state를 사용한다면, 공통 컴포넌트나 그 부모 컴포넌트에 state를 위치시킨다.
  • state 캡슐화: state가 소유하고 설정한 컴포넌트 이외에는 어떠한 컴포넌트에도 접근할 수 없다. 지난 번에 자세히 언급하지 않았으나 중요하다.

► 이번 학습: 역방향 데이터 흐름

위의 내용을 종합해보면 하향식 데이터 흐름, 단방향 데이터 흐름이 중요하며 state, props를 통해 컴포넌트를 효과적으로 연결할 수 있다.
그런데 위의 내용과 연결하여 역방향 데이터 흐름도 존재한다.

  • 역방향 데이터 흐름
    부모 컴포넌트에서의 상태가 하위 컴포넌트에 의해 변할 때를 의미한다.
    이를 해결하기 위해서는 State 끌어올리기(Lifting state up)이 필요하다.
  • State 끌어올리기(Lifting state up)
    위의 역방향 데이터 흐름을 해결하기 위해, 상태를 변경시키는 setState 함수를 하위 컴포넌트에 props로 전달하는 것이다.
    아래는 State 끌어올리기 예제이다.
    주소 state를 주로 해당 컴포넌트에 사용하고 그 내부에서 변경했으나,
    이 예제처럼 부모 컴포넌트에서 state를 props로 넘겨주고 자식 컴포넌트에서 부모 컴포넌트의 값을 바꾸도록 할 수 있다.
function Parent() {
  const [parentState, setParentState] = useState("기본값");

  const handleChangeState = (newState) => {
    setParentState(newValue);
  };

  return (
    <div>
      <div>현재 state값은 {parentState} 입니다</div>
      <Child handleChangeState={handleChangeState}  />
    </div>
  );
}

function Child({ handleChangeState }) {
  const clickEventHandle = () => {
    handleChangeState("자식이 보내는 값");
  }

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

관련 내용: 🔗React 공식문서 | 5.state와 생명주기 - 데이터는 아래로 흐릅니다

► 정리: React App 설계 방법

위의 복습한 내용과 역방향 데이터 흐름에 대해 정리하자면 리액트 애플리케이션의 설계 방법은 다음과 같다.

  1. 목업으로 시작하기
  2. UI를 컴포넌트 계층 구조로 나누기
  3. React로 정적인 버전 만들기
  4. UI state에 대한 최소한의 (하지만 완전한) 표현 찾아내기
  5. State가 어디에 있어야 할 지 찾기
  6. 역방향 데이터 흐름 추가하기

자세한 내용은 공식문서에 나와있다.
🔗React 공식문서 | React로 사고하기

Ch2. Effect Hook

Side Effect (부수 효과)

부수 효과란 함수 내부의 구현이 함수 외부에 영향을 주는 경우를 의미한다.
특히 리액트에서의 부수 효과는 다음과 같다.

  • 타이머 사용 (setTimeout)
  • 데이터 가져오기 (fetch API, localStorage, DOM 조작 이벤트)

Pure Function (순수 함수)

순수 함수란 함수의 입력에 따라 같은 결과가 나오며 함수 내부에서만 영향을 주는 함수를 의미한다.
즉 반대로 입력 외의 값이 함수 결과에 영향을 미치거나 부수 효과를 발생시킬 때 순수 함수라고 할 수 없다.
정리하자면

순수 함수의 조건

  • 함수의 결과가 함수의 입력이 아닌 다른 값에 영향을 받지 않는다.
  • 입력으로 전달된 값을 수정하지 않는다.
  • 전달인자가 일정하면 항상 똑같은 결과가 리턴된다.(예측가능)

예시)

  • Math.random()은 순수 함수가 아니다. 입력은 같아도 결과가 항상 다르기 때문이다.
  • 타이머, 네트워크 요청, localstorage 사용은 순수 함수가 아니다. 부수효과를 발생시킨다. React와 관련없는 API를 사용하기 때문이다.
  • React의 함수 컴포넌트는 순수 함수이다. 입력은 props, 출력은 JSX element며 부수 효과가 발생하지 않기 때문이다.

Hook 사용시 주의점

공식문서에 따르면 Hook은 JavaScript 함수이나 사용할 때 다음과 같이 주의해야 한다.

  • 최상위에서만 Hook을 호출해야 한다.
    반복문, 조건문 혹은 중첩된 함수 내에서 Hook을 호출하지 말아야 한다. 그래야 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 Hook이 호출되고 useState 와 useEffect 가 여러 번 호출되어도 Hook의 상태를 올바르게 유지할 수 있도록 한다.
    🔗관련 공식문서 예제
  • React 함수 내에서 Hook을 호출해야 한다.
    일반적인 JavaScript 함수에서 호출하지 말고 리액트의 함수 컴포넌트나 custom Hook에서 Hook을 호출해야 한다.

Effect Hook

Effect Hook은 컴포넌트 내에서 부수 효과를 실행할 수 있게 하는 Hook으로 useEffect가 있다.
사용법은 아래와 같다.

useEffect(함수, [조건을담은배열])
  • useEffect 첫 번째 인자: 함수
    해당 함수 내에서 side effect를 실행한다.
  • useEffect 두 번째 인자: 배열
    조건(변경이 될 값)을 담은 배열이다.
    이 조건은 boolean 형태가 아니다. 배열에는 상태가 변경될 값이 요소로 들어간다.
    즉 배열의 요소들이 변경될 때마다 첫번째 인자인 함수가 실행된다.
    이 배열을 '종속성 배열(dependency array)'이라고 한다.

useEffect 실행 조건

인자에 따라 useEffect의 첫번째 인자의 함수 실행 조건이 달라진다.

  • useEffect(함수)
    컴포넌트가 새롭게 렌더링될 때마다 실행된다.
    컴포넌트가 렌더링 될 때는 아래와 같다.
    • 첫 화면에 컴포넌트 생성된 후
    • 컴포넌트에 새로운 props가 전달될 때
    • 컴포넌트에 상태(state)가 바뀔 때
  • useEffect(함수, [])
    함수가 한 번만 실행된다.
    주로 API를 통해 리소스를 응답받고 더 이상 API 호출이 필요하지 않을 때 사용한다.
  • useEffect(함수, [요소1, 요소2])
    종속성 배열의 요소1 값이 변하거나 요소2 값이 변할 때 함수가 실행된다.

Ch3. 컴포넌트 내의 AJAX 요청

데이터 처리 방식의 차이점

검색 결과 등 검색하려는 데이터가 있는지 검색하기위해 데이터를 요청해서 접근해야 한다. 이 때
데이터를 한 번에 받아서 접근할지,
그때마다 데이터를 요청하여 접근할지
선택할 수 있다.

첫번째는 클라이언트에서, 두 번째는 매번 서버에 요청해야 한다.
두 방식은 아래와 같이 차이가 있다.

  1. 클라이언트에서 처리(컴포넌트 내부)
  • 장점: HTTP 요청의 빈도를 줄일 수 있다.
  • 단점: 브라우저(클라이언트)의 메모리에 많은 데이터를 저장하게 된다.
  1. 서버에 매번 요청하여 처리(컴포넌트 외부)
  • 장점: 클라이언트에서 필터링 구현이 쉽다.
  • 단점: 빈번한 HTTP 요청이 일어난다.

이를 예제로 통해 확인해볼 수 있다.

검색한 결과를 화면에 나타내는 예제

input태그에 어떤 값을 입력할 때마다 그 문자열이 들어간 도시명을 나타내는 예제가 있다.
이 예제를 살펴보면 처음 언급한 데이터 처리 방식이 어떤식으로 이뤄지는지 좀 더 쉽게 알 수 있다.

import { useEffect, useState } from "react";

//외부의 도시 데이터: stringify를 이용해 저장한다.
localStorage.setItem(
  "city",
  JSON.stringify([
    "Seoul",
    "Paris",
    "New York",
    "Milano",
    "Tokyo"
  ])
);

//외부의 데이터 처리하기
function getCity(searchingValue = "") {
  const jsonData = localStorage.getItem("city");
  const cities = JSON.parse(jsonData) || [];
  return cities.filter((el) => el.toLowerCase().includes(searchingValue.toLowerCase())
  );
}

export default function App() {
  const [cities, setCities] = useState([]);
  const [searching, setSearching] = useState("");
  const [count, setCount] = useState(0);

  //1. 데이터를 불러오는 방식에 따라 인자를 추가로 입력한다.
  useEffect(() => {
    const result = getCity(searching);
    setCities(result);
  });
  
  const handleChange = (e) => {
    setSearching(e.target.value);
  };
  
  return (
    <div className="App">
      도시명을 검색하세요
      <input type="text" value={searching} onChange={handleChange} />
      <ul>
        {/* 2. 데이터를 불러오는 방식에 따라 이 부분이 달라진다 */}
      </ul>    
    </div>
  );
}

function City(props) {
  return <li>{props.city}</li>;
}

이 예제는 위 데이터 처리 방식에 따라 1번 부분, 2번 부분을 다르게 구현해볼 수 있다.

컴포넌트 내부에서 처리하기

1번 부분

  //1. 데이터를 불러오는 방식에 따라 인자를 추가로 입력한다.
  useEffect(() => {
    const result = getCity();
    setCities(result);
  }, []); //클라이언트 내부에 모든 데이터를 처음 한 번만 받아와 실행한다.

2번 부분

//내부에서 데이터를 받아 직접 필터링 처리한다.
<ul>
  {cities
    .filter((el) => {
    return el.toLowerCase().includes(searching.toLowerCase());
  })
    .map((el, i) => (
    <City city={el} key={i} />
  ))}
</ul>  

컴포넌트 외부에서 처리하기

1번 부분

  //1. 데이터를 불러오는 방식에 따라 인자를 추가로 입력한다.
  useEffect(() => {
    const result = getCity(searching);
    setCities(result);
  }, [searching]); //searching값이 변할때마다 함수가 실행된다.

2번 부분

//값이 변할 때마다 필터링되어 저장되므로 렌더링만 한다.
<ul>
  {cities.map((el, i) => (
    <City city={el} key={i} />
  ))}
</ul>

AJAX 요청

위의 예제를 fetch API를 써서, 임의의 서버(http://서버주소/cities)에 요청한다고 가정하자.
그리고 예제를 바꿔보면 아래와 같다.

useEffect(() => {
  fetch(`http://서버주소/cities?q=${searching}`)
    .then(resp => resp.json())
    .then(result => {
      setCities(result);
    });
}, [searching]);

Loading indicator란?

보통 AJAX 요청이 느릴 경우를 대비해 로딩중이라는 의미의 화면을 구현한다.
이 로딩 화면에는 크게 두가지가 있다.

  • loading indicator
    다음처럼 로딩중임을 나타내는 아이콘을 사용할 수 있다.

출처: https://flutterawesome.com/a-collection-of-high-fidelity-loading-animations-in-gif-format-with-flutter/

  • loading placeholder
    보통 벨로그나 유튜브를 접속했을 때 접속이 안되거나, 내용을 불러오기 전 잠깐 볼 수 있는 화면이다.
    회색으로 처리된 부분들이 지속적으로 하얗게 빛나는 경우도 있다.

Loading indicator 불러오기

이 로딩 인디케이터를 불러올 때
state를 사용해 미리 구현해둔 Loading indicator 컴포넌트를 불러올 수 있다.

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

//LoadingIndicator: 로딩 인디케이터 컴포넌트
//Content: 로딩 완료 후 나타날 화면 컴포넌트
export default function App(){
  return {isLoading ? <LoadingIndicator /> : <Content />}
}

그리고 fetch API를 활용해 로딩 인디케이터를 구현할 수 있다.

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

추가학습

1. props와 구조분해 할당
컴포넌트에서 부모로부터 전달받은 props를 사용할 때 구조분해할당을 사용할 수 있다.
본문의 도시 검색 예제를 보면 아래와 같이 props를 사용한 것을

function City(props) {
  return <li>{props.city}</li>;
}

아래처럼 구조분해할당을 사용해 표현할 수 있다.

function City({city}) {
  return <li>{city}</li>;
}

구조분해할당이므로 원래 prop가 있던 자리의 중괄호는 props가 객체이기에 사용한 중괄호이다. 때문에 중괄호 안의 키 이름은 props에 부모가 지정한 속성이름으로 저장된 키 이름이여야 한다.

profile
프론트엔드 개발자 NH입니다. 시리즈로 보시면 더 쉽게 여러 글들을 볼 수 있습니다!

0개의 댓글