코드스테이츠_S2U9_8W_화,수

윤뿔소·2022년 10월 11일
0

CodeStates

목록 보기
25/47

오늘은 상태 끌어올리기와 Effect Hook 사용법을 알고 컴포넌트 내 Ajax 호출까지 배워보자!
알아야 할 것: state이 아닌 것
- 부모로부터 props를 통해 전달되나?
- 시간이 지나도 변하지 않나?
- 컴포넌트 안의 다른 state나 props를 가지고 계산 가능한가?

상태 끌어올리기

  • Lifting STATES Up 이라고들 한다.
  • 저번에 리액트는 상향식 구조로 컴포넌트를 짜면 더 수월하다고 배웠다! 트리구조로 이뤄져 각각 컴포넌트를 취합하는 식으로 설계하면 좋다.
  • 그렇다면! 단일 책임 원칙에 따른 구분으로 <NewTweetForm>, <Tweets>-<SingleTweet> 이렇게 나눠 입력, 데이터 렌더링 하는 컴포넌트를 나눈 단순한 구조의 컴포넌트 디자인이다.
    • 리액트의 데이터 흐름은 기본적으로 단방향, 하향식이므로 <Tweets>-<SingleTweet>구조, 즉! 부모가 자식에게 데이터를 주는 구조
    • 특정 상태에 기반하여 여러개의 컴포넌트가 사용해야되니 State를 사용하면 편함
    • 이렇게 다 정했는데 여기서 입력창에서 입력하고 '제출 버튼'을 누르면 전체 트윗 목록에 입력된 데이터가 들어가야하는 디자인을 짜야하는 게 생겼다?!
    • 그래서 하위컴포넌트의 클릭 이벤트가 부모의 상태를 바꾼다. 엥? 역방향이다?! 즉! 자식이 부모에게 영향을 끼치는 상황이 온 것이다.
    • 여기서 '상태 끌어올리기' 스킬이 등장한다!

      상위 컴포넌트의 "상태를 변경하는 함수" 그 자체를 하위 컴포넌트로 전달하고, 이 함수를 하위 컴포넌트가 실행하는 스킬
      props(props={Value})를 이용하여 콜백처럼 사용하자!

예시

여기 이 문제가 있다. 한번 풀어보자! 부모는 Twittler, 자식은 NewTweetForm 등등이 있다.

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 />
      <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에 전달
  };

  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;

위에서 설명한 것처럼 자식과 부모간 역방향으로 데이터를 주기에 그렇게 조작해주자
1. 부모: return에 넣어 자식을 렌더링하는데 자식의 콩고물(파라미터)를 쓰고 원하는 곳에 대입

function Twittler() {
  return (
    <div>
      <div>작성자: {currentUser}</div>
      {/* NewTweetForm의 props(파라미터) onButtonClick을 addNewTweet에 적용시켜줌 */}
      <NewTweetForm onButtonClick={addNewTweet} />
      <ul id="tweets">
        {tweets.map((t) => (
          <SingleTweet key={t.uuid} writer={t.writer} date={t.date}>
            {t.content}
          </SingleTweet>
        ))}
      </ul>
    </div>
  );
}
  1. NewTweetForm: 제출 클릭했을때 렌더링되는 데이터에 추가되기
function NewTweetForm({ onButtonClick }) {
  const onClickSubmit = ({}) => {
    let newTweet = {
      uuid: Math.floor(Math.random() * 10000),
      writer: currentUser,
      date: new Date().toISOString().substring(0, 10),
      content: newTweetContent
    };
    // TDOO: 여기서 newTweet이 addNewTweet에 전달
    // NewTweetForm의 파라미터 onButtonClick을 가져와 newTweet을 삽입
    onButtonClick(newTweet);
  };
};

Side Effect

함수 내에서 어떤 구현이 함수 외부에 영향을 끼치는 경우(부수효과)를 뜻함

  • 즉! 컴포넌트의 순수 함수적인 특징에 위배됨!
    • 로그인, 가입 등 서버에 요청인 API-fetch
    • DOM을 이용한 이벤트리스너 같은 것들
  • ⭐️React 컴포넌트를 만들 때 side effect로부터 분리해서 생각하기!

참고: 순수 함수 예시

const obj = {value: 12};
function sum(obj, num){
	return {value: obj.value + num};
}

이 코드는 함수가 실행되도 기존 obj나 obj.value에게 영향을 안끼쳐서 순수 함수임

let c = 12;
function sum(a, b){
	c = a * b;
	return a + b;
}

얘는 함수가 실행되면 될 수록 변수 c에 영향을 줌 그래서 순수 함수가 아님

useEffect

렌더링될 때 함수를 실행하게 만들어주는 Hook
이걸 이용해 side effect를 실행할 수 있다!?

⭐️useEffect API

useEffect(()=>{}, [실행 조건(옵션)1, 2, ...])

  • 그렇다면 언제 컴포넌트가 렌더링되고 함수가 실행될까?
    1. 컴포넌트 생성 후 처음 화면에 렌더링(표시)
    2. 컴포넌트에 새로운 props가 전달되며 렌더링
    3. 컴포넌트에 상태(state)가 바뀌며 렌더링
  • 실행 조건 관련
    • 없다면(디폴트값) 위 3가지 조건 그대로 실행
    • 실행 조건이 빈배열이면 컴포넌트가 생성 후 렌더링될 때 함수가 딱 한번 실행됨
    • 실행 조건이 여러개를 써서 종속 변수(state 같은)들이 변할 때 마다 useEffect 사용 가능
  • 주의점
    • 최상위에서만 Hook을 호출하기
    • React 함수 내에서 Hook을 호출하기

useEffect with fetch

서버에서부터 가져오는 fetch와 useEffect를 섞어 어떤 이벤트를 발생하면 그때 서버에서 데이터를 가져오는 기술도 쓸 수 있음!

참고 : 명언 필터 처리기

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

예시

위의 파일들은 이제 하드코딩으로 가져왔지만 만약 서버에서 수십만개의 명언을 요청한다면?

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

로딩도 필요!

서버에서 가져올 때 로딩 이미지를 붙여줘 가져오고있다는 표시를 하면 EX적인 부분에서 만족을 줄 수 있다!

// 로딩을 설정할 State 구현
const [isLoading, setIsLoading] = useState(true);
// 생략, LoadingIndicator 컴포넌트는 별도로 구현했음을 가정합니다
return {isLoading ? <LoadingIndicator /> : <div>로딩 완료 화면</div>}
// 위 코드와 조합하여 filter의 값이 바뀔 때 마다 로딩과 fetch로 서버에서 가져옴
useEffect(() => {
  setIsLoading(true);
  fetch(`http://서버주소/proverbs?q=${filter}`)
    .then(resp => resp.json())
    .then(result => {
      setProverbs(result);
      setIsLoading(false);
    });
}, [filter]);

과제: States Airline Client

이제 과제를 통해 상태 끌올, useEffect를 활용하자!

.디렉토리 구조
├── README.md
├── __tests__
│   └── index.test.js # 테스트 파일
├── api
│   └── FlightDataApi.js # 항공편 정보를 받아오는 API
├── package.json
├── pages
│   ├── Main.js # 첫 화면 컴포넌트, 필터링 상태를 담고 있습니다.
│   ├── component
│         ├── Debug.js            # 디버그용 컴포넌트 (테스트 통과에 필요합니다)
│         ├── Flight.js           # 단일 항공편
│         ├── FlightList.js       # 항공편 목록
│         ├── LoadingIndicator.js # 로딩 컴포넌트
│         └── Search.js           # 검색 도구 컴포넌트, 필터링 상태를 변경합니다.
├── public
├── resource
│   └── flightList.js # 하드코딩된 항공편 정보
└── styles
    └── globals.css # 스타일 시트

상태 끌어올리기

Main.js와 Search.js 간 연결하기
1. condition은 출발지와 도착지를 담을 수 있음
2. search 함수는 condition을 변경하는 함수
3. 자식인 <Search />컴포넌트의 input 입력값을 부모인 Main.js의 search 함수와 연결시키는 구조

Main.js

// 모듈 중략
export default function Main() {
  // 항공편 검색 조건을 담고 있는 상태: useState 중략
  // 주어진 검색 키워드에 따라 condition 상태를 변경시켜주는 함수
  const search = ({ departure, destination }) => {
    if (condition.departure !== departure || condition.destination !== destination) {
      console.log("condition 상태를 변경시킵니다");

      // search 함수가 전달 받아온 '항공편 검색 조건' 인자를 condition 상태에 적절하게 담아보자
      setCondition({ departure, destination });
    }
  };
// filterByCondition(): 필터해주는 함수 중략
  // search 함수를 Search 컴포넌트로 내려주기
  return (
    // head 부분 중략
      <main>
        <h1>여행가고 싶을 땐, States Airline</h1>
    	// Search 컴포넌트를 props로 search함수를 고정함
        <Search onSearch={search} />
        <div className="table">
          <div className="row-header">
            <div className="col">출발</div>
            <div className="col">도착</div>
            <div className="col">출발 시각</div>
            <div className="col">도착 시각</div>
            <div className="col"></div>
          // 중략

Search.js

// import 생략
// 파라미터 설정
function Search({ onSearch }) {
  // input 설정 함수 생략
  const handleSearchClick = () => {
    console.log("검색 버튼을 누르거나, 엔터를 치면 search 함수가 실행");
    // 상위 컴포넌트에서 props를 받아서 실행시켜 보자.
    // Search의 파라미터를 가져와줌
    onSearch({ departure: "ICN", destination: textDestination });
  };
// return 및 export 생략

결론

아~ 잘 짝지어줄려면 부모와 자식 관계를 명확히 알아야하고 부모에게 제대로 props를 설정, 자식도 받아주는 파라미터를 확실히 설정해서 이어주는거구나~

useEffect

상태를 끌올해서 condition이 바뀌면 검색 조건도 바뀌게 설정됐다!

  1. 그런데 이번엔 filterByCondition()를 빼고 api를 불러와 필터하게 만드는, flight라는 params와 그 뒤 ?=query를 이용한, api/FlightDataApi.js에 담긴 getFlight(condition)함수를 만들자.
  2. 당연히 api를 불러오기에 fetch를 사용하는데, fetch는 대표적인 Side Effect 함수로 컴포넌트와 별개로 불러오기에 useEffect를 잘 설정해야한다. 만든 getFlight()를 이용해서 만들자!
  3. 추가로 로딩을 집어넣는 방법도 모색해서 EX의 수준을 한단계 올리자!

fetch하기

api/FlightDataApi.js

// 파라미터에 '='은 디폴트 파라미터로 입력되지 않았을 시 입력 되게끔, 오류가 안나게끔 
export function getFlight(filterBy = {}) {
  // API를 사용하기 위해, fetch를 이용. 아래 구현은 getFlight(filterBy = {})을 하드코딩한 것
  // TODO: 아래 구현을 REST API 호출로 대체
  return fetch(
    `http://ec2-13-124-90-231.ap-northeast-2.compute.amazonaws.com:81/flight?departure=ICN&destination=${filterBy.destination}`
  ).then((response) => response.json());
// getFlight() 하드코딩 주석 처리 및 중략
}
  1. Props인 filterBy를 써서 그 조건의 데이터를 받아와 Ajax요청을 해 JSON 파일을 받아옴
  2. ~compute.amazonaws.com:81/flight?인 API를 GET하면 데이터가 날라옴
  3. params인 flight? 뒤에 query인 조건들을 붙여 데이터를 가져옴: 필터되는 것임!!
  4. departure는 ICN으로 고정, destination은 Props로 받아와 condition에 입력된 것을 가져오기(State)
  5. ⭐️.json()은 fetch의 응답 메소드이며 fetch는 기본적으로 Promise타입 객체(JSON)를 반환하기에 response.json()을 해줘 JS 객체로 쓰일 수 있게끔 설정

참고: .json()의 반환값은 사실 JS 객체로 나오지 않는다!

미친 포인트를 잡아버렸다.
이상한 점이 있었는데 .json()은 JS 객체로 반환해준다고 했는데 위 사진을 보면 결국 Promise를 반환하는 청개구리짓을 한다. 이 새끼 왜이래?!

이거에 대한 답은 Promise의 작동 방식에 있다. fetch를 한 후에 날아오는 response 객체는 모든 header가 도착하자마자 우리에게 주어진다. 즉, header만 올 뿐 body가 아직 오지 않는다는 의미다. 그래서 그다음 .then~console.log같은 then 체이닝 등으로 비동기 과정을 하나 더 거쳐야 JS 객체가 드디어 온다.

즉! fetch.json()을 입력하면 해당 body 값을 '기다리고 있는 상태'인 Promise 객체를 반환-'리턴'한다!

useEffect 사용하기

Main.js

import { useEffect, useState } from "react";
import { getFlight } from "../api/FlightDataApi";
export default function Main() {
  const [condition, setCondition] = useState({
    departure: "ICN",
  });
  const [flightList, setFlightList] = useState(getFlight);
// search 함수, filterByCondition 함수 중략
  useEffect(async () => {
    setFlightList(await getFlight(condition));
  }, [condition]);
  return (
    // 중략
    <Search onSearch={search} />
    {/* <FlightList list={flightList.filter(filterByCondition)} /> */}
    <FlightList list={flightList} />
  1. useEffect를 사용해 컴포넌트가 실행되고, condition(Dependency Array, 실행 조건)가 변경되면 State 변경 함수인 setFlightList()에 FlightDataApi파일을 import해온 getFlight() 함수를 가져왔고, condition을 넣어 걸러지게끔 설정했다!
  2. ⭐️그런데 async, await 등 비동기를 왜 넣었을까? 왜냐면 위 참고에서 봤듯이 fetch에 .json을 바로 해버린 상태면 Promise 객체가 나오고, useEffect와 setFlightList는 Promise를 받지 못 async, await로 JS 객체를 제대로 반환 받아야 setFlightList에 적용할 수 있다.
  3. return에 하드코딩은 주석 처리해주고, <FlightList list={flightList} />을 넣어 Props도 적용케 했다.

로딩 넣어주기

로딩 기능도 넣어주자 EX를 향상시키기 위해!

LoadingIndicator.js

당연히 gif 이미지도 추가돼있음

function LoadingIndicator() {
  return (
    <img
      className="loading-indicator"
      alt="now loading..."
      src="loading.gif"
      style={{ margin: '1rem' }}
    />
  );
}
export default LoadingIndicator;

Main.js

  1. 로딩이 됐는지, 안됐는지를 설정하기위한 State 추가
  2. 로딩이 어떤 조건에 바뀌는지 setIsLoading를 어디에 배치? 렌더링 되면 로딩이 끝나게!
  3. 로딩 State에 따라 뷰포트에 렌더링 O? X?
import LoadingIndicator from "./component/LoadingIndicator";
export default function Main() {
  // 상태 부분: 기본값 true
  const [isLoading, setIsLoading] = useState(true);
  // useEffect는 렌더링을 해주는 기능이니 useEffect의 순서에 따라 isLoading가 true, false로 
  useEffect(async () => {
    setIsLoading(true);
    setFlightList(await getFlight(condition));
    setIsLoading(false);
  }, [condition]);
  // 조건문을 통해서 true면 로딩이 렌더링, false면 none되게
  return (
  // 중략
    {/* <FlightList list={flightList.filter(filterByCondition)} /> */}
    {/* <FlightList list={flightList} /> */}
    {isLoading ? <LoadingIndicator /> : <FlightList list={flightList} />}

핵심: return으로 렌더링 설정해주는 코드가 있는데 여기에선 무조건 삼항 연산자가 必, 그리고 로딩 상태는 기본값으로 true가 좋다.(왜인지는 모르겠다.. stackoverflow에 안나온다..), useEffect와 같이 쓰이니 알아두면 너무 좋다~

profile
코뿔소처럼 저돌적으로

0개의 댓글