6-4. 리스트 데이터 추가하기 (CREATE)

2022. 7. 4. 16:21React/한입 크기로 잘라 먹는 리액트(React.js)

그래서 React는 단방향 데이터 흐름

그래서 state를 에디터와 리스트에 공통 분모 요소로 끌어올려서 해결할 수 있다.

editor와 list의 공통분모인 App 컴포넌트가 일기 데이터를 state로 배열형식의 state를 가지고있고

data state 의 값을 다이어리 리스트에게 전달하면서, 리스트를 렌더링하게 하고

데이터라는 state를 변화시킬 수 있는 setData라는 상태변화함수를 editor 컴포넌트로 프롭으로 전달해주면 된다.

 

Simulating

https://hanamon.kr/codestates-til-%ED%95%AD%ED%95%B4%EC%9D%BC%EC%A7%80-35%EC%9D%BC%EC%B0%A8/

 

[TIL] 항해일지 35일차 - 하나몬

⚡️ React 데이터 흐름 ❗️컴포넌트로 생각하기 👉 React의 개발 방식의 특징 페이지 단위가 아닌, 컴포넌트 단위로 시작한다. 먼저 컴포넌트를 만들고, 다시 페이지로 조립한다. 즉, 상향식(botto

hanamon.kr

더보기

⚡️ React 데이터 흐름

❗️컴포넌트로 생각하기

👉 React의 개발 방식의 특징

  • 페이지 단위가 아닌, 컴포넌트 단위로 시작한다.
  • 먼저 컴포넌트를 만들고, 다시 페이지로 조립한다.
  • 즉, 상향식(bottom-up)으로 앱을 만든다.

👉 상향식(bottom-up) 개발 방법의 장점

  • 테스트가 쉽고 확장성이 좋다.

👉 컴포넌트 만드는 방법

  • 프로토타입을 보고 가장 먼저 해야할 일은 컴포넌트를 계층 구조로 나누는 것이다.
  • 하나의 컴포넌트는 한가지 일만 하도록 단일 책임 원칙에 따라 구분해야한다.

❗️데이터는 위에서 아래로 흐른다.

데이터 흐름이 하향식(top-down)이다.

  • 단방향 데이터 흐름(one-way data flow)이라는 키워드가 React를 대표하는 설명 중 하나일 정도로 이 원칙은 매우 중요하다.
  • 데이터를 전달하는 주체는 부모 컴포넌트가 된다.
  • 컴포넌트는 컴포넌트 바깥에서 props를 이용해 데이터를 마치 인자(arguments) 혹은 속성(attributes)처럼 전달받을 수 있다.
  • 컴포넌트는 props를 통해 전달받은 데이터가 어디서 왔는지 전혀 알지 못한다.

❗️데이터 정의

  • 애플리케이션에서 필요한 데이터가 무엇인지 먼저 정의한다.
  • 이 중에 변하는 값과, 변하지 않는 값은 무엇인지 찾는다.
  • 확실히 사용자 입력은 이벤트에 따라 얼마든지 변할 수 있다.
  • 이것은 상태(state)이다.
  • 게시글 목록 역시 새 게시글 추가라는 기능을 염두해두고 있으므로, 상태(state)이다.

❗️상태 위치 정하기

  • 상태가 특정 컴포넌트에서만 유의미하다면, 특정 컴포넌트에만 두면 되니까 크게 어렵지 않지만, 만일 하나의 상태를 기반으로 두 컴포넌트가 영향을 받는다면 이 때에는 공통 소유 컴포넌트를 찾아 그 곳에 상태를 위치해야 한다.
  • 두 개의 자식 컴포넌트가 하나의 상태에 접근하고자 할 때는 두 자식의 공통의 부모 컴포넌트에 상태를 위치해야 한다.

❗️역방향 데이터 흐름 추가

  • 앞서 리액트는 단방향 데이터 흐름을 갖고 있다고 하였다.
  • 그런데, 갑자기 왠 역방향 데이터 흐름이라는 이야기가 나올까?

부모 컴포넌트에서의 상태가 하위 컴포넌트에 의해 변하는 상황이 있을 수 있다.

  • 버튼 클릭 시 바로 새로운 게시글 추가가 대표적인 예다.
  • 버튼을 통한 이 액션은, 부모의 상태를 변화시켜야 한다.

❗️상태 끌어 올리기

  • 하위 컴포넌트에서의 클릭 이벤트가, 부모의 상태를 바꾸어야만 하는 상황은 어떻게 해결할 수 있을까?
  • 키워드는 바로 “State 끌어올리기(Lifting state up)”이다.

상태를 변경시키는 함수(handler)를 하위 컴포넌트에 props로 전달해서 해결할 수 있다.

  • 이는 마치 콜백 함수를 사용하는 방법과 비슷하다.

 

참고 : React로 사고하기 – React

참고 : State and Lifecycle – React

 


 

⚡️ State 끌어올리기 (Lifting State Up)

  • 단방향 데이터 흐름이라는 원칙에 따라, 하위 컴포넌트는 상위 컴포넌트로부터 전달받은 데이터의 형태 혹은 타입이 무엇인지만 알 수 있다.
  • 데이터가 state로부터 왔는지, 하드코딩으로 입력한 내용인지는 알지 못한다.
  • 그러므로 하위 컴포넌트에서의 어떤 이벤트로 인해 상위 컴포넌트의 상태가 바뀌는 것은 마치 “역방향 데이터 흐름”과 같이 조금 이상하게 들릴 수 있다.
  • React가 제시하는 해결책은 다음과 같습니다.

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

 


 

⚡️ Side Effect

❗️Side Effect (부수 효과)

  • 함수 내에서 어떤 구현이 함수 외부에 영향을 끼치는 경우 해당 함수는 Side Effect가 있다고 이야기한다.
  • 다음은, 전역 변수 a를 sampleFn라는 함수가 수정하는 예제이다.
let a = 'hello';
 
function sampleFn () {
a = 'world';
}
 
sampleFn(); // sampleFn는 Side Effect를 발생시킨다.

 

❗️Pure Function (순수 함수)

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

 

❗️React 컴포넌트에서의 Side Effect

  • React 애플리케이션을 작성할 때에는 AJAX 요청이 필요하거나, LocalStorage 또는 타이머와 같은
    React와 상관없는 API를 사용하는 경우가 발생할 수 있다.
  • 이는 React의 입장에서는 전부 Side Effect 이다.

React는 Side Effect를 다루기 위한 Hook Effect Hook을 제공한다.

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

 

 


 

 

⚡️ Effect Hook (useEffect)

❗️Effect Hook (기본 형태)

useEffect(함수)

  • useEffect의 첫 번째 인자는 함수이다.
  • useEffect의 첫 번째 인자는 함수 내에서 Side Effect를 실행하면 된다.
  • useEffect의 첫 번째 인자는 함수는 다음과 같은 조건에서 실행된다.

👉 실행 조건

  • 컴포넌트 생성 후 처음 화면에 렌더링
  • 컴포넌트에 새로운 props가 전달되며 렌더링
  • 컴포넌트에 상태(state)가 바뀌며 렌더링

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

 

❗️Effect Hook (조건부 실행)

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

  • useEffect의 두 번째 인자는 배열이다.
  • useEffect의 두 번째 인자인 배열은 종속성 배열이다.
  • useEffect의 두 번째 인자인 배열은 조건을 담고 있다.
    • 여기서 조건은 Boolean 형태의 표현식이 아니다.
    • 여기서 조건은 어떤 값의 변경이 일어날 때를 의미한다.
    • 따라서, 해당 배열엔 어떤 값의 목록이 들어간다.
    • 이러한 배열을 특별히 종속성 배열(Dependency Array)이라고 부른다.

👉 실행 조건

  • 배열 내의 어떤 값이 변할 때에만, (effect가 발생하는) 함수가 실행된다.
  • 예를 들어 배열 내의 종속성1 또는 종속성2의 값이 변할 때, 첫번째 인자의 함수가 실행된다.

👉 단 한번만 실행되는 Effect 함수

  • 만일 종속성 목록에 아무런 종속성도 없다면 어떤 일이 발생할까?
  • 달리 말해, 두번째 배열을 빈 배열[]로 둘 경우에는 무슨일이 발생할까?
  • 두번째 인자를 아예 안넘기는 것과 어떻게 다를까?

1. 아무것도 넣지 않기 (기본 형태)
useEffect(함수)

  • useEffect의 두 번째 인자가 없는 경우 (기본 형태)
  • 컴포넌트가 처음 생성되거나, props가 업데이트되거나, 상태(state)가 업데이트될 때 effect 함수가 실행된다.

2. 빈 배열 넣기
useEffect(함수, [])

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

 

❗️Hook을 쓸 때 주의할 점

 


 

질문!

useEffect는 해당 컴포넌트 내에서 side effect가 발생하면 실행된다는 것인가?
useEffect의 첫 번째 인자인 함수가 해당 컴포넌트에 side effect를 만들어 낸다는 것인가?

답변 정리!

컴포넌트 라이프 사이클 후크’에 대한 이해부터 함수형 컴포넌트에서의 ‘라이프 사이클 후크’인 useEffect에 대한 자세한 개념을 알 수 있었습니다.

1. useEffect는 해당 컴포넌트 내에서 side effect가 발생하면 실행된다는 것인가?는
라이프 사이클이라는 개념과 useEffect후크 사용법에 대해 알게되면서 아닌 것으로 깨달았습니다.
useEffect는 사용방법에 따라, 컴포넌트 생명주기와 관련해서 발생한다는 것이라는 것을 알았습니다.

2. useEffect의 첫 번째 인자인 함수가 해당 컴포넌트에 side effect를 만들어 낸다는 것인가?는
맞다라는 것을 알았습니다.
이유는 후크라는 것이 해당 라이프 사이클에서 잠깐! 하고 끼어드는 것 처럼 발생을 하는데
이때 useEffect의 첫 번째 인자인 함수의 구현에 따라 side effect를 발생 시킬 수도 있기 때문이다.

 


 

Data Fetching: 필터링 예제

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

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

두 방식의 차이점

컴포넌트 내부에서 처리 :
장점 : HTTP 요청의 빈도를 줄일 수 있다.
단점 : 브라우저(클라이언트)의 메모리 상에 많은 데이터를 갖게 되므로, 클라이언트의 부담이 늘어난다.

컴포넌트 외부에서 처리 :
장점 : 클라이언트가 필터링 구현을 생각하지 않아도 된다.
단점 : 빈번한 HTTP 요청이 일어나게 되며, 서버가 필터링을 처리하므로 서버가 부담을 가져간다.

App.js

import {useRef, useState} from 'react';
import DiaryEditor from './DiaryEditor';
import DiaryList from './DiaryList';
import "./App.css";

// const dummyList = [
//   {
//     id: 1,
//     author: "이정환",
//     content: "하이 1",
//     emotion: 5,
//     created_date: new Date().getTime(),
//   },
//   {
//     id: 2,
//     author: "홍길동",
//     content: "하이 2",
//     emotion: 2,
//     created_date: new Date().getTime(),
//   },
//   {
//     id: 3,
//     author: "아무개",
//     content: "하이 3",
//     emotion: 3,
//     created_date: new Date().getTime(),
//   },
// ];

const App = () =>  {
  // 전역적으로 Data 관리할 state
  const [data, setData] = useState([]);
  const dataId = useRef(0);

  const onCreate = (author, content, emotion) => {
    const created_date = new Date().getTime();
    const newItem = {
      author,
      content,
      emotion,
      created_date,
      id: dataId.current
    };
    dataId.current += 1;
    setData([newItem, ...data]); // 새로운 일기를 제일 위로 보내기 위해서
  };

  return (
    <div className="App">
      <DiaryEditor onCreate={onCreate} />
      <DiaryList diaryList={data} />
    </div>
  );
}

export default App;

DiaryList.js

import DiaryItem from "./DiaryItem";

const DiaryList = ({ diaryList }) => {
  return (
    <div className="DiaryList">
      <h2>일기 리스트</h2>
      <h4>{diaryList.length}개의 일기가 있습니다.</h4>
      <div>
        {diaryList.map((it) => (
          <DiaryItem key={it.id} {...it} />
        ))}
      </div>
    </div>
  );
};

// defaultProps는 undefined로 전달 될 수 있는 값을 default로 설정해주는 것
DiaryList.defaultProps = {
    diaryList: []
}

export default DiaryList;

DiaryEditor.js

import React, { useRef, useState } from "react";

const DiaryEditor = ({ onCreate }) => {
  const authorInput = useRef();
  const contentInput = useRef();

  const [state, setState] = useState({
    author: "",
    content: "",
    emotion: 1,
  });

  const handleChangeState = (e) => {
    setState({
      ...state,
      [e.target.name]: e.target.value,
    });
  };

  const handleSubmit = () => {
    if (state.author.length < 1) {
      // focus
      authorInput.current.focus();
      return;
    }
    if (state.content.length < 5) {
      contentInput.current.focus();
      // focus
      return;
    }
    onCreate(state.author, state.content, state.emotion);
    alert("저장 성공");
    setState({
      author: "",
      content: "",
      emotion: 1
    })
  };

  return (
    <div className="DiaryEditor">
      <h2>✨오늘의 일기✨</h2>
      <div>
        <input
          ref={authorInput}
          name="author"
          value={state.author}
          onChange={handleChangeState}
        />
      </div>
      <div>
        <textarea
          ref={contentInput}
          name="content"
          value={state.content}
          onChange={handleChangeState}
        />
      </div>
      <div>
        <span>오늘의 감정점수 : </span>
        <select
          name="emotion"
          value={state.emotion}
          onChange={handleChangeState}
        >
          <option value={1}>1</option>
          <option value={2}>2</option>
          <option value={3}>3</option>
          <option value={4}>4</option>
          <option value={5}>5</option>
        </select>
      </div>
      <div>
        <button onClick={handleSubmit}>일기 저장하기</button>
      </div>
    </div>
  );
};
export default DiaryEditor;