react 데이터 흐름

react에서의 데이터 흐름

컴포넌트로 생각하기

react의 개발 방식의 가장 큰 특징인 컴포넌트 단위(페이지 단위x)로 시작한다는 점을 다시 떠올려 보자
→ 페이지를 만들기 이전에, 컴포넌트를 먼저 만들고 조립(상향식으로 앱을 만듬)
→ 테스트가 쉽고, 확장성이 좋음

 

단일 책임 원칙: 하나의 컴포넌트는 한가지 일을 처리하는 게 이상적이다

 

데이터는 위에서 아래로 흐른다(데이터 흐름은 하향식)

컴포넌트는 컴포넌트 바깥에서 props를 이용해 데이터를 마치 인자(arguments) 혹은 속성(attributes)처럼 전달 받을 수 있다


→ 데이터를 전달하는 주체: 부모 컴포넌트
→ 부모 컴포넌트는 자신의 state를 자식 컴포넌트에 props로 전달할 수 있다

 

부모가 자식에게 데이터를 전달하는 단방향 데이터 흐름(one-way data flow)
→ 자식 컴포넌트는 props를 통해 전달받은 데이터가 어디서 왔는지를 전혀 알지 못한다

모든 state는 항상 특정한 컴포넌트가 소유하고 있으며 그 state로부터 파생된 UI 또는 데이터는 오직 트리구조에서 자신의 “아래”에 있는 컴포넌트에만 영향을 미친다

데이터 정의

어떤 데이터를 상태(state)로 두어야 할 지를 잘 생각해야 한다
→ 상태는 최소화하는 게 좋기 때문!

 

어떤 데이터를 상태로 두어야 하는지 여부에 대한 판단

1. 부모로부터 props를 통해 전달 받았는가?
→ 맞다면, `확실히` state가 아님

2. 시간이 지나도 변하지 않는가?
→ 맞다면, `확실히` state가 아님

3. props를 가지고 계산 가능한가?
→ 맞다면, state가 아니다(100퍼는 아님)

상태 위치 정하기

두 개의 서로 다른 컴포넌트가 특정 상태에 의존적일 경우?
→ 공통 소유 컴포넌트(공통의 부모)에다가 상태를 위치해야 한다

 

애플리케이션이 가지는 각각의 state에 대해서

 

state를 기반으로 렌더링하는 모든 컴포넌트를 찾으세요.
공통 소유 컴포넌트 (common owner component)를 찾으세요. (계층 구조 내에서 특정 state가 있어야 하는 모든 컴포넌트들의 상위에 있는 하나의 컴포넌트).
공통 혹은 더 상위에 있는 컴포넌트가 state를 가져야 합니다.
state를 소유할 적절한 컴포넌트를 찾지 못하였다면, state를 소유하는 컴포넌트를 하나 만들어서 공통 오너 컴포넌트의 상위 계층에 추가하세요.

역방향 데이터 흐름 추가

부모 컴포넌트에서의 상태가 하위 컴포넌트에 의해서 변하는 모습

 

ex) 트위터에서 트윗 추가
→ 트윗 버튼을 통한 액션은 부모의 상태를 변화시킨다
→ 하위 컴포넌트에서의 클릭 이벤트가 부모의 상태를 바꾸어야만 하는 상황

 

이를 해결하는 키워드는 state 끌어올리기(Lifting state up)

상태(state) 끌어올리기(Lifting state up)

상태를 변경시키는 함수를 하위 컴포넌트에 props로 전달해서,
하위 컴포넌트는 이를 통해 상태 끌어올리기가 가능
→ 마치 콜백 함수처럼 함수 자체를 props로 전달하는 것(부모가 자식한테)

 

콜백(callback) 다시보기

다른 함수(고차 함수)의 인자로 전달되는 함수

 

이를 react 예제로 살펴 보자
부모와 자식 컴포넌트가 하나씩 존재하고, 상태를 변경시킬 수 있는 메소드가 존재한다고 생각

function Parent() {
  const [value, setValue] = useState("변경되는 값");

  const handleChangeValue = () => {    // 상태를 변경하는 함수
    setValue("이 값으로 변경되었음");
  };

  return (
    <div>
      <div>값은 {value} 입니다</div>
      <Child handleButtonClick={handleChangeValue}  />    //props이름은 알아서…
    // 여기서는 props 이름을  handleButtonClick로 지었음
    </div>
  );
}

 

 자식 컴포넌트(Child)는 고차함수가 인자로 받은 함수를 실행하듯이, props로 전달받은 함수를 컴포넌트 내에서 실행할 수 있게 된다
상태 변경 함수는 버튼이 클릭할 때 실행되기를 원하므로 해당 부분에서 콜백함수를 실행

function Child({ handleButtonClick }) {
  const handleClick = () => {
    // Q. 이 버튼을 눌러서 부모의 상태를 바꿀 순 없을까?
    // A. 인자로 받은 상태 변경 함수를 실행하자

    handleButtonClick()    // 콜백함수를 실행
  }

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

 

필요에 따라서는 설정할 값을 콜백함수의 인자로 넘길 수도 있다

function Parent() {
  const [value, setValue] = useState("변경되는 값");

  const handleChangeValue = (newValue) => {    //받은 인자로
    setValue(newValue);    // 값을 설정
  };

  // ...생략...
}

function Child({ handleButtonClick }) {
  const handleClick = () => {
    handleButtonClick('내가 원하는 값은 이거야')    // 인자를 넣음
  }

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

react 공식문서에서도 관련 실습을 할 수 있다
https://ko.reactjs.org/docs/lifting-state-up.html


Effect Hook

Side Effect

Side Effect(부수 효과)

함수 내에서 어떤 구현이 함수 외부에 영향을 끼치는 경우
해당 함수는 Side Effect가 있다 라고 함

ex) 전역 변수 foo를 bar라는 함수가 수정하는 예

let foo = 'hello';

function bar() {
  foo = 'world';
}

bar(); // bar는 Side Effect를 발생시킴    // 함수 내부 구현이 함수 외부의 foo를 변화시키므로

Pure Function(순수 함수)

오직 함수의 입력만이 함수의 결과의 영향을 주는 함수
→ 입력으로 전달된 값을 수정하지 않는다
→ Side Effect(ex. 네트워크 요청)가 없다

 

순수함수는 어떠한 전달 인자가 주어진 경우, 항상 똑같은 값이 리턴됨을 보장한다
예측 가능한 함수

 

cf) 함수의 입력이 아닌 다른 값이 함수의 결과에 영향을 미치는 경우→ 순수함수 x

Q. Math.random()은 순수 함수가 아니다. Why?
A. 항상 똑같은 값이 리턴되는 것을 보장하지 않으니깐?

Q. 어떤 함수가 fetch API를 이용해 AJAX 요청을 한다고 가정해봅시다. 이 함수는 순수 함수가 아니다. Why?
A. 네트워크 요청과 같은 Side Effect를 발생시키기 때문?

react의 함수 컴포넌트

여태까지의 기본적인 형태의 함수 컴포넌트들은 props가 입력으로, JSX Element가 출력으로 나갔다
→ 순수함수로 동작했음

ex) 기본적인 형태의 react 함수 컴포넌트
// 순수함수로 동작(side effect가 x)

function SingleTweet({ writer, body, createdAt }) {
  return <div>
    <div>{writer}</div>
    <div>{createdAt}</div>
    <div>{body}</div>
  </div>
}

 

하지만, 보통 react 앱을 작성할 때에는 react와 상관없는 API를 사용하는 경우가 존재한다
→ AJAX 요청, LocalStorage, 타이머 사용 등
→ 이는 react 입장에서는 전부 Side Effect!

 

react에서는 이러한 Side Effect를 다루기 위해 Effect Hook를 제공한다

react 컴포넌트에서의 Side Effect의 예

* 타이머 사용(setTimeout 등)

* 데이터 가져오기(fetch API, localStorage 등)

Effect Hook – 기본

useEffect: 컴포넌트 내에서 Side Effect를 실행할 수 있게 하는 Hook

ex) 브라우저 API를 이용하여 타이틀(제목)을 변경하는 Side Effect의 예

import { useEffect, useState } from "react";
import "./styles.css";

export default function App() {
const proverbs = [
"좌절감으로 배움을 늦추지 마라",
"Stay hungry, Stay foolish",
"Memento Mori",
"Carpe diem",
"배움에는 끝이 없다"
];
const [idx, setIdx] = useState(0);

const handleClick = () => {
setIdx(idx === proverbs.length - 1 ? 0 : idx + 1);
};

return (
<div className="App">
<button onClick={handleClick}>명언 제조</button>
<Proverb saying={proverbs[idx]} />
</div>
);
}

function Proverb({ saying }) {
useEffect(() => {            //useEffect 사용 부분
document.title = saying;
});
return (
<div>
<h3>오늘의 명언</h3>
<div>{saying}</div>
</div>
);
}

API

useEffect(함수)

 

첫번째 인자: 함수
→ 첫번째 인자인 해당 함수 내에서 Side Effect를 실행하면 됨

Effect Hook은 언제 실행되는가?

* 컴포넌트 생성: 컴포넌트 생성 후 처음 화면에 렌더링(표시)

* 새 props: 컴포넌트에 새로운 props가 전달되며 렌더링

* state 변경: 컴포넌트에 상태(state)가 바뀌며 렌더링

 

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

Hook 사용 시 지켜야 할 2가지 규칙


1. 최상위(at the Top Level)에서만 Hook을 호출해야 한다:
반복문, 조건문 혹은 중첩된 함수 내에서 Hook을 호출하면 안된다. 
대신 early return이 실행되기 전에 항상 react 함수의 최상위에서 Hook을 호출해야 한다.

→ 이는 React가  useState 와 useEffect 가 여러 번 호출되는 중에도 Hook의 상태를 
올바르게 유지할 수 있도록 해 준다.

2. 오직 react 함수 내에서 Hook을 호출해야 한다
Hook을 일반적인 JavaScript 함수에서 호출하면 안된다. 대신 아래와 같이 호출해야 한다
- react 함수 컴포넌트에서 Hook을 호출
- Custom Hook에서 Hook을 호출

→ 이는 컴포넌트의 모든 상태 관련 로직을 소스 코드에서 명확하게 보이도록 해 준다.

eslint-plugin-react-hooks 플러그인을 설치하면 이 2가지 규칙을 강제시킬 수 있다.

 

이 플러그인은 Create React App에 기본적으로 포함되어 있다.

npm  설치
# npm npm install eslint-plugin-react-hooks --save-dev

------------

// ESLint 설정 파일
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
    "react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
  }
}

react는 hook이 호출하는 순서에 의존하므로, hook을 조건문 안에서 호출한다면, hook 호출 순서가 달라질 수 있다. 그러면 버그가 발생할 수 있다

→ 이 것이 컴포넌트 최상위에서 Hook이 호출되어야만 하는 이유
→ 조건부로 effect를 실행하기를 원한다면 조건문을 Hook 내부에 넣으면 됨

 

ex) 조건문 안에서 hook을 호출한 예

// 조건문에 Hook을 사용함으로써 첫 번째 규칙이 깨짐
  if (name !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
  }


---------

ex) Hook 내부에서 조건문을 사용한 예

useEffect(function persistForm() {
    // 더 이상 첫 번째 규칙을 어기지 않음
    if (name !== '') {
      localStorage.setItem('formData', name);
    }
  });

Effect Hook - 조건부 실행

조건부 effect 발생(dependency array)

useEffect의 두번째 인자는 배열(array)이다. 이 배열을 조건을 담고 있는 배열
→ 여기서 조건 이란 boolean 형태의 표현식이 아닌, 어떤 값의 변경이 일어날 때를 의미함
→ 즉, 이 배열에는 어떤 값의 목록 이 들어감
→ 이 배열을 종속성 배열(dependency array) 라고 부른다

 

API

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

useEffect의 두번째 인자인 종속성 배열
→ 배열 내의 종속성1, 또는 종속성2 등의 값이 변할 때, 첫번째 인자의 함수가 실행된다

 

배열 내의 어떤 값이 변할 때에만 (side effect가 발생하는) 첫번째 인자의 함수가 실행되는 것

단 한번만 실행되는 Effect 함수의 case

종속성 목록(종속성 배열)에 아무런 종속성도 없다면
→ 즉, 두번째 배열이 빈 배열 []인 경우와 두번째 인자를 아예 안 넘길 때의 차이?

1. 빈 배열 넣기

useEffect(함수, [])

→ 이 경우엔, 컴포넌트가 처음 생성될 때에만 effect 함수가 실행된다
→ 단 한번, 외부 API를 통해 리소스를 받아오고 더이상 API 호출이 필요하지 않은 경우에 사용

2. 두번째 인자에 아무것도 넣지 않기(기본 형태)

useEffect(함수)

→ 일반적인 형태이므로 기존에 effect hook가 실행되는 3가지 case에 의해서 실행됨

컴포넌트 내에서 AJAX 요청

Data Fetching

필터링 예제를 통해서 알아보자

해당 예제에서 목록 내 필터링을 구현하기 위해서는 2가지 접근 방식이 존재

 

1. 컴포넌트 내에서 필터링: 전체 목록 데이터를 불러오고, 목록을 검색어로 filter하는 방법

2. 컴포넌트 외부에서 필터링: 컴포넌트 외부로 API 요청을 할 때, 필터링한 결과를 받아오는 방법
→ ex) 서버에 매번 검색어와 함께 요청하는 경우

컴포넌트 내부에서 필터링

처음 단 한번만, 외부 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();    //외부 API인 getProverbs에서 목록을 가져옴
setProverbs(result);
}, []);    // useEffect의 두번째 인자인 배열을 빈 배열 [] 로 둠

const handleChange = (e) => {
setFilter(e.target.value);
};

return (
<div className="App">
필터
<input type="text" value={filter} onChange={handleChange} />
<ul>
{proverbs    // 가져와서 반영된 값인 proverbs를 이용
.filter((prvb) => {    // filter 함수를 이용
return prvb.toLowerCase().includes(filter.toLowerCase());
})
.map((prvb, i) => (
<Proverb saying={prvb} key={i} />
))}
</ul>
</div>
);
}

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

컴포넌트 외부에서 필터링

검색어가 바뀔 때마다, 외부 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]);    // 종속성 배열에 입력한 filter값(종속성 값들)을 넣음 → filter 값이 바뀔때마다
// 외부 API인 getProverbs가 호출됨

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

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

두 방식의 차이점?

만약 서버에서 수십만 개의 데이터를 제공한다고 가정해 보자

 

HTTP를 이용한 서버 요청을 가정할 때의 비교

* 컴포넌트 내부에서 처리
- 장점: HTTP 요청의 빈도를 줄일 수 있음
- 단점: 브라우저(클라이언트)의 메모리 상에 많은 데이터를 가지게 되므로, 클라이언트의 부담이 증가

* 컴포넌트 외부에서 처리
- 장점: 클라이언트가 필터링 구현 등을 생각하지 않아도 됨
- 단점: 빈번한 HTTP 요청이 발생하고, 서버가 필터링 등을 처리하므로 서버의 부담이 증가

AJAX 요청 보내기

fetch API를 사용하여 서버에 요청을 하는 예시

// http://서버주소/proverbs 를 엔드포인트라고 가정

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

AJAX 요청이 매우 느릴 경우에는?

외부 API 접속이 느릴 경우를 고려하여, 로딩 화면(loading indicator)의 구현도 필수적으로 해야 한다

 

로딩 화면의 구현은 어떻게 해야 하는가?
→ 여기에도 상태 처리 가 필요하다

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

// LoadingIndicator 컴포넌트는 별도로 구현했음을 가정

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

 

이렇게 로딩 화면에 대한 코드 작성을 하였으면, 이를 fetch 요청의 에 설정(setIsLoading) 해줘서 보다 나은 사용자 경험(UX)를 구현할 수 있다

useEffect(() => {
  setIsLoading(true);    // 로딩되고 있으므로, `로딩 화면`을 출력(true)
  fetch(`http://서버주소/proverbs?q=${filter}`)
    .then(resp => resp.json())
    .then(result => {
      setProverbs(result);
      setIsLoading(false);    // fetch가 끝났으므로, 로딩이 완료된 화면을 출력(false)
    });
}, [filter]);

추가적인 보충 내용

 

실무에서는 어느 한쪽에 부담이 아닌, 서버의 부담과 클라이언트의 부담이 적절하게 분배된 애플리케이션 구조로 만드는 경우가 많다.


복사했습니다!