React 클라이언트 Ajax 요청

이성은·2022년 12월 6일
0
post-custom-banner

1.React 데이터 흐름

학습목표

  • React에서의 데이터 흐름, 단방향 데이터 흐름을 이해할 수 있다.
  • 어떤 컴포넌트에 state가 위치해야 하는지 알 수 있다.
  • State 끌어올리기의 개념을 이해할 수 있다.
    • 상태 변경 함수가 정의된 컴포넌트와, 상태 변경 함수를 호출하는 컴포넌트가 다름을 알 수 있다.
  • React 데이터 흐름
    • 상향식(bottom-up)제작
      : 컴포넌트를 먼저 만들고, 페이지에 맞게 컴포넌트를 조립 => 확장성, 테스트 용이하다.
    • 단방향 데이터 흐름
      :컴포넌트는 컴포넌트 바깥에서 props를 이용해 데이터를 마치 전달인자(arguments) 혹은 속성(attributes)처럼 전달받을 수 있다. => 부모 컴포넌트는 자신의 state를 자식 컴포넌트에 props로 전달
  • 컴포넌트간의 상호작용, 데이터의 흐름 등을 고려하여 데이터 선언 위치를 설정한다.

1.디자인을 전달 받으면 가장 먼저 컴포넌트 계층 구조를 분석한다.
=> 하나의 기능을 가진 컴포넌트로 구분
2.데이터의 전달 위치 설정한다.
=> 데이터의 흐름(하향식 top-down)을 고려하여 부모 컴포넌트에서 Props 방식으로 데이터 전달
3.state를 비롯한 필요한 데이터 정의 및 구분
=> state는 최소화하는 것이 좋다.
[X] 부모 컴포넌트에서 Props로 전달되는가?
[X] 시간이 지나도 변하지 않는가?
[X] 컴포넌트 안의 다른 state나 props로 계산 가능한가?
-> state가 아닌 일반 데이터
4.state의 위치 결정 , 역방향 데이터 흐름 추가
하나의 state가 여러개의 컴포넌트에 영향을 끼칠 경우, 공통되는 부모 컴포넌트에서 선언하고 영향을 끼치지 않으면 사용하는 컴포넌트에서 선언
=> 작성중인 상태를 나타내는 state는 새로운 글을 작성하는 컴포넌트에서 정의하여 사용.
(새로운 트윗 추가 -> 부모가 가진 전체트윗목록(state)상태를 변화시킴
하위 컴포넌트(NewTweetForm)에서의 클릭 이벤트가, 부모의 상태를 바꾸어야만 하는 상황)
반면 게시글 목록을 표현하는 state는 새글을 추가하는 컴포넌트의 영향을 받으므로 게시글 목록 컴포넌트보다 공통적인 부모 컴포넌트에서 state를 정의하여 사용.

1-2. State 끌어올리기

  • 상위 컴포넌트의 "상태를 변경하는 함수" 그 자체를 하위 컴포넌트로 전달하고, 이 함수를 하위 컴포넌트가 실행한다.

  • props로 state 대신 '상태변경함수 Handler' 를 전달

  • 콜백 함수처럼 함수 자체를 props로 전달 → 하위 컴포넌트가 실행
    => 하위 컴포넌트에서 전달받은 상태변경함수를 실행하면 state 변경되며 단반향 데이터 흐름 원칙도 유지

  • 예제 1

    • 상태 갱신 함수는 setValue, 상태를 변경하는 함수는 handleChangeValue
    • props로 handleChangeValue를 자식 컴포넌트에 전달할 것
    • props의 이름은 handelButtonClick, 이것을 자식 컴포넌트의 {인자}로 넣는다.
    • 자식 컴포넌트내에 handelButtonClick를 실행시킬 함수블록 내에 넣어준다

<부모 컴포넌트>

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

  // 상태를 변경하는 함수
  const handleChangeValue = () => {
    setValue("보여줄게 완전히 달라진 값");
  };
  	{/* 설정할 값을 콜백 함수의 인자로 넘길 수도 있음
    
	const handleChangeValue = (newValue) => {
    setValue(newValue);
  };
  
	*/}
  
  return (
    <div>
      <div>값은 {value} 입니다</div>
      {/* props를 이용하여 상태 변경 함수를 하위 컴포넌트로 전달 */}
      <ChildComponent handleButtonClick={handleChangeValue}  />
    </div>
  );
}

<자식 컴포넌트>

// 인자로 받은 상태 변경 함수를 실행
function ChildComponent({ handleButtonClick }) {
  const handleClick = () => {
    handleButtonClick()
  };
  
  {/*   설정할 값을 콜백 함수의 인자로 넘길 수도 있음
   
	const handleClick = () => {
    handleButtonClick('넘겨줄게 자식이 원하는 값')
  }
  
	*/}
  
  return <button onClick={handleClick}>값 변경</button>;
}
  • 예제2 : 자식 값을 전달하는 방법
    <부모 컴포넌트>
import React, { useState } from "react";
import "./styles.css";

const currentUser = "김코딩";

function Twittler() {
  const [tweets, setTweets] = useState([
    {
      uuid: 1,
      writer: "김코딩",
      date: "2020-10-10",
      content: "안녕 리액트"
    },
    {
      uuid: 2,
      writer: "박해커",
      date: "2020-10-12",
      content: "좋아 코드스테이츠!"
    }
  ]);

  const addNewTweet = (newTweet) => {
    setTweets([...tweets, newTweet]);
  }; // 이 상태 변경 함수가 NewTweetForm에 의해 실행되어야 합니다.

  return (
    <div>
      <div>작성자: {currentUser}</div>
      <NewTweetForm onButtonClick={addNewTweet}/> // props로 전달
      <ul id="tweets">   
        {tweets.map((t) => (
          <SingleTweet key={t.uuid} writer={t.writer} date={t.date}>
            {t.content}
          </SingleTweet>
        ))}
      </ul>
    </div>
  );
}

<자식 컴포넌트>

function NewTweetForm({ onButtonClick }) {
  const [newTweetContent, setNewTweetContent] = useState("");

  const onTextChange = (e) => {
    setNewTweetContent(e.target.value);
  };

  const onClickSubmit = () => {
    let newTweet = {
      uuid: Math.floor(Math.random() * 10000),
      writer: currentUser,
      date: new Date().toISOString().substring(0, 10),
      content: newTweetContent
    };
    // TDOO: 여기서 newTweet이 addNewTweet에 전달되어야 합니다.
    onButtonClick(newTweet);
  };

  return (
    <div id="writing-area">
      <textarea id="new-tweet-content" onChange={onTextChange}></textarea>
      <button id="submit-new-tweet" onClick={onClickSubmit}>  
        새 글 쓰기
      </button>
    </div>
  );
}

<자식 컴포넌트>

function SingleTweet({ writer, date, children }) {
  return (
    <li className="tweet">
      <div className="writer">{writer}</div>
      <div className="date">{date}</div>
      <div>{children}</div>
    </li>
  );
}

export default Twittler;

2. Effect Hook

학습 목표

  • Side effect가 어떤 의미인지 알 수 있다.
  • React 컴포넌트를 만들 때 side effect로부터 분리해서 생각할 수 있다. (비즈니스 로직과 표현 영역 구분)
    • Side effect의 예를 들 수 있다.
  • Effect Hook을 이용해 비동기 호출 및 AJAX 요청과 같은 side effect를 React 컴포넌트 내에서 처리할 수 있다.
  • Effect Hook에서의 dependency array 사용법을 이해할 수 있다.
  • 컴포넌트 내에서 네트워크 요청 시, 로딩 화면과 같이 보다 나은 UI를 만드는 법을 이해할 수 있다.

2-1. Side Effect

  • Side Effect (부수 효과)
    • 함수 내에서 어떤 구현이 함수 외부에 영향을 끼치는 경우 해당 함수는 Side Effect가 있다고 이야기한다.
    • React에서는 컴포넌트 내에서 fetch를 사용해 API 정보를 가져오는 경우, 이벤트를 활용해 DOM 직접 조작할때
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'
  • 질문
    • Math.random()은 순수 함수가 아닙니다. 왜일까요?
      => Math.random은 어떤 결과값이 나올지 예측이 불가능하기 때문에 순수 함수가 아니다.
    • 어떤 함수가 fetch API를 이용해 AJAX 요청을 한다고 가정해 봅시다. 이 함수는 순수 함수가 아닙니다. 왜일까요?
      =>네트워크 상황, 서버상태에 따라 응답코드가 달라져서 예측이 불가능하기 때문에 순수 함수가 아니다.
  • React의 함수 컴포넌트
    • 앞서 배운 React의 함수 컴포넌트는, props가 입력으로, JSX Element가 출력, 여기에는 그 어떤 Side Effect도 없으며, 순수 함수로 작동한다.
    • 타이머 사용 (setTimeout)
    • 데이터 가져오기 (fetch API, localStorage)
      • AJAX 요청이 필요하거나, LocalStorage 또는 타이머와 같은 React와 상관없는 API를 사용하는 경우
    • React에서는 Effect Hook을 통해 side effect를 핸들링한다.

2-2. Effect Hook 기본

  • useEffect : 컴포넌트 내에서 Side effect를 실행할 수 있게 하는 Hook
  • useEffect의 첫 번째 인자 : 함수
    • 해당 함수 내에서 side effect를 실행
  • 매번 새롭게 컴포넌트가 렌더링 될 때 Effect Hook이 실행
    • 컴포넌트 생성 후 처음 화면에 렌더링(표시)
    • 컴포넌트에 새로운 props가 전달되며 렌더링
    • 컴포넌트에 상태(state)가 바뀌며 렌더링
  • Hook을 쓸 때 주의할 점
    • 최상위에서만 Hook을 호출한다.
    • React 함수 내에서 Hook을 호출한다.

    2-3. Effect Hook 조건부 실행

  • useEffect(함수, [종속성1, 종속성2, ...])
  • useEffect의 두 번째 인자 : 배열(어떤 값의 변경이 일어날 때의 조건을 담고 있다.)
    => 해당 배열엔 어떤 값의 목록이 들어간다. 이 배열을 특별히 종속성 배열이라고 부른다.
  • 배열 내의 종속성1, 또는 종속성2의 값이 변할 때, 첫 번째 인자의 함수가 실행된다.
  • 배열 내의 어떤 값이 변할 때에만, (effect가 발생하는) 함수가 실행된다.

    < useEffect 3가지 형태 >

  • 인자로 콜백함수만 있는경우(기본형태)
 useEffect(()=>{
 console.log(몇 번 호출될까요?)
})

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

  • 인자로 콜백함수+빈배열
useEffect(()=>{
 console.log(몇 번 호출될까요?)
}, [])

컴포넌트가 처음 생성될 때만 단 한 번만, effect 함수가 실행
대표적으로 처음 단 한 번, 외부 API를 통해 리소스를 받아오고 더 이상 API 호출이 필요하지 않을 때에 사용할수있다.

  • 인자로 콜백함수+특정state변수
useEffect(()=>{
 console.log(몇 번 호출될까요?)
}, [count])

컴포넌트가 최초 마운트될때(화면에 나타날때), state인 count가 업데이트 될 때마다 실행

2-4. 컴포넌트 내에서의 Ajax 요청

  • Data Fetching :목록 내 필터링을 구현
    1. 컴포넌트 내에서 필터링: 전체 목록 데이터를 불러오고, 목록을 검색어로 filter 하는 방법
// 컴포넌트 내에서 필터링
// 처음 단 한 번, 외부 API로부터 명언 목록을 받아오고, filter 함수를 이용
import { useEffect, useState } from "react";
import "./styles.css";
import { getProverbs } from "./storageUtil";

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

  useEffect(() => {
    console.log("언제 effect 함수가 불릴까요?");
    const result = getProverbs();
    setProverbs(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>;
}

//LocalStorage API를 이용
// storageUtil.js 
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())
  );
}
  1. 컴포넌트 외부에서 필터링: 컴포넌트 외부로 API 요청을 할 때, 필터링 한 결과를 받아오는 방법 (보통, 서버에 매번 검색어와 함께 요청하는 경우가 이에 해당)
// 컴포넌트 외부에서 필터링
// 검색어가 바뀔 때마다, 외부 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);
    setProverbs(result);
  }, [filter]);

  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) => (
          <Proverb saying={prvb} key={i} />
        ))}
      </ul>
      <button onClick={handleCounterClick}>카운터 값: {count}</button>
    </div>
  );
}

function Proverb({ saying }) {
  return <li>{saying}</li>;
}

//LocalStorage API를 이용
// storageUtil.js 
/*setItem() :localStorage에 아이템을 추가하기 위해서는 setItem() 함수를 사용
window.localStorage.setItem(key, value)*/
localStorage에 아이템을 추가하기 위해서는 setItem() 함수를 사용합니다.
localStorage.setItem(
  "proverbs",
  JSON.stringify([
    "좌절감으로 배움을 늦추지 마라",
    "Stay hungry, Stay foolish",
    "Memento Mori",
    "Carpe diem",
    "배움에는 끝이 없다"
  ])
);

export function getProverbs(filterBy = "") {
  const json = localStorage.getItem("proverbs");
  /*getItem() :localStorage의 아이템을 읽기 위해서는 getItem() 함수를 사용
  window.localStorage.getItem(key)*/
  const proverbs = JSON.parse(json) || [];
  return proverbs.filter((prvb) =>
    prvb.toLowerCase().includes(filterBy.toLowerCase())
  );
}

=> 두 방식의 차이점

  • AJAX 요청보내기
    : 임의로 구현한 storageUtil.js 대신, fetch API를 써서 서버에 요청
    => 명언을 제공하는 API의 엔드포인트가 http://서버주소/proverbs 라고 가정
//fetch API를 써서 서버에 요청
useEffect(() => {
  fetch(`http://서버주소/proverbs?q=${filter}`)
    .then(resp => resp.json())
    .then(result => {
      setProverbs(result);
    });
}, [filter]);
  • AJAX 요청이 매우 느릴 경우
    :모든 네트워크 요청이 항상 즉각적인 응답을 가져다주는 것은 아니다. 외부 API 접속이 느릴 경우를 고려하여, 로딩 화면(loading indicator)의 구현은 필수적
    • 로딩화면 구현 => 상태 처리가 필요
  • 에러 화면도 구현 필요
//fetch API를 써서 서버에 요청
//나머지 생략하고 로딩 화면이랑 에러 화면 구현

const [loading, setLoading] = useState(false);
  //처음에는 로딩 안됀 상태로 시작
  const [error, setError] = useState(undefined);
  //처음에는 에러가 없는 상태
  //그냥 useState()해도 처음에는 undefined할당

useEffect(() => {
  fetch(`http://서버주소/proverbs?q=${filter}`)
    .then(resp => resp.json())
    .then(result => {
      setProverbs(result);
    })
    .catch((e) => setError("에러가 발생했음"))
      .finally(() => setLoading(false));
    //데이터를 받아왔던 안받아왔던 로딩은 끝을 내야해
}, [filter]);

 if (loading) return <p>loading..</p>;
 if (error) return <p>{error}</p>;
profile
함께 일하는 프론트엔드 개발자 이성은입니다🐥
post-custom-banner

0개의 댓글