[React] 심화(2)

이성은·2023년 1월 25일
0
post-custom-banner

2-4. Custom Hooks

  • React는 변경된 새로운 가상 DOM 트리에 부합하도록 기존의 UI를 효율적으로 갱신하는 방법, 즉 하나의 트리를 다른 트리로 변형을 시키는 가장 작은 조작 방식을 알아냄
    =>O(n^3)의 복잡도를 가짐,너무 비싼 연산
  • React는 두 가지의 가정을 가지고 시간 복잡도 O(n)의 새로운 휴리스틱 알고리즘(Heuristic Algorithm)을 구현해냄
    • 상태관리 로직의 재활용이 가능하고
    • 클래스 컴포넌트보다 적은 양의 코드로 동일한 로직을 구현할 수 있으며
    • 함수형으로 작성하기 때문에 보다 명료하다는 장점이 있다.
  • 아래 두 컴포넌트에는 정확하게 똑같이 쓰이는 로직이 존재 하고 있다.
    => 이 로직을 빼내서 두 컴포넌트에서 공유
    =>Custom Hook을 사용
//FriendStatus : 친구가 online인지 offline인지 return하는 컴포넌트
function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

//FriendListItem : 친구가 online일 때 초록색으로 표시하는 컴포넌트
function FriendListItem(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}
  • Custom Hook으로 useFriendStatus만듬
    • Custom Hook을 정의할 때는 함수 이름 앞에 use를 붙이는 것이 규칙이다.
    • 대개의 경우 프로젝트 내의 hooks 디렉토리에 Custom Hook을 위치 시킨다.
    • Custom Hook으로 만들 때 함수는 조건부 함수가 아니어야 한다. 즉 return 하는 값은 조건부여서는 안 된다. 그렇기 때문에 위의 이 useFriendStatus Hook은 온라인 상태의 여부를 boolean 타입으로 반환하고 있다.
function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}
  • useFriendStatus Hook을 두 컴포넌트에 적용
    • 그러나 같은 Custom Hook을 사용했다고 해서 두 개의 컴포넌트가 같은 state를 공유하는 것은 아니다. 그저 로직만 공유할 뿐, state는 컴포넌트 내에서 독립적으로 정의 되어 있다.
function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}
  • 예제1. 여러 url을 fetch할 때 쓸 수 있는 useFetch Hook
const useFetch = ( initialUrl:string ) => {
	const [url, setUrl] = useState(initialUrl);
	const [value, setValue] = useState('');

	const fetchData = () => axios.get(url).then(({data}) => setValue(data));	

	useEffect(() => {
		fetchData();
	},[url]);

	return [value];
};

export default useFetch;
  • 예제2. 여러 input에 의한 상태 변경을 할 때 쓸 수 있는 useInputs Hooks
import { useState, useCallback } from 'react';

function useInputs(initialForm) {
  const [form, setForm] = useState(initialForm);
  // change
  const onChange = useCallback(e => {
    const { name, value } = e.target;
    setForm(form => ({ ...form, [name]: value }));
  }, []);
  const reset = useCallback(() => setForm(initialForm), [initialForm]);
  return [form, onChange, reset];
}

export default useInputs;
  • 실습1. custom hook을 이용하여 useEffect 로직 분리하기
    • App 컴포넌트에 들어있는 useEffect hook 로직을 util 폴더 내의 hooks.js 파일에 분리해 보고, 직관적으로 파악할 수 있도록 hooks.js의 이름도 변경해 보자.
//로직 분리하기 전에 App.js
import "./styles.css";
import { useEffect, useState } from "react";

export default function App() {
  const [data, setData] = useState();

  useEffect(() => {
    fetch("data.json", {
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json"
      }
    })
      .then((response) => {
        return response.json();
      })
      .then((myJson) => {
        setData(myJson);
      })
      .catch((error) => {
        console.log(error);
      });
  }, []);

  return (
    <div className="App">
      <h1>To do List</h1>
      <div className="todo-list">
        {data &&
          data.todo.map((el) => {
            return <li key={el.id}>{el.todo}</li>;
          })}
      </div>
    </div>
  );
}
//App.js
import "./styles.css";
import useFetch from "./util/use-fetch";
export default function App() {
  const fetchData = useFetch("data.json");

  return (
    <div className="App">
      <h1>To do List</h1>
      <div className="todo-list">
        {fetchData &&
          fetchData.todo.map((el) => {
            return <li key={el.id}>{el.todo}</li>;
          })}
      </div>
    </div>
  );
}
//src폴더안에 util파일 따로 만듬
//util파일안에 hooks.js로 use-fetch.js 파일 따로 만듬
//use-fetch.js :useEffect hook 로직 따로 분리
import { useEffect, useState } from "react";

export default function useFetch(fetchUrl) {
  const [data, setData] = useState();
  useEffect(() => {
    fetch(fetchUrl, {
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json"
      }
    })
      .then((response) => {
        return response.json();
      })
      .then((myJson) => {
        setData(myJson);
      })
      .catch((error) => {
        console.log(error);
      });
  }, [fetchUrl]);
  return data;
}

//data.json
{
  "todo": [
    { "id": 1, "todo": "lunch" },
    { "id": 2, "todo": "study" },
    { "id": 3, "todo": "go for a walk" }
  ]
}
  • 실습2. custom hook을 이용하여 input 로직 분리하기
    • App 컴포넌트에 들어 있는 input을 Input 컴포넌트로 바꾼 후, custom hook을 이용하여 input의 로직을 분리해보자.
// 이전 코드, App.js
import { useState } from "react";
import "./styles.css";

export default function App() {
  const [firstNameValue, setFirstNameValue] = useState("");
  const [lastNameValue, setLastNameValue] = useState("");
  const [nameArr, setNameArr] = useState([]);

  const handleSubmit = (e) => {
    e.preventDefault();
    setNameArr([...nameArr, `${firstNameValue} ${lastNameValue}`]);
  };

  return (
    <div className="App">
      <h1>Name List</h1>
      <div className="name-form">
        <form onSubmit={handleSubmit}>
          <div className="name-input">
            <label></label>
            <input
              value={firstNameValue}
              onChange={(e) => setFirstNameValue(e.target.value)}
              type="text"
            />
          </div>
          <div className="name-input">
            <label>이름</label>
            <input
              value={lastNameValue}
              onChange={(e) => setLastNameValue(e.target.value)}
              type="text"
            />
          </div>
          <button>제출</button>
        </form>
      </div>
      <div className="name-list-wrap">
        <div className="name-list">
          {nameArr.map((el, idx) => {
            return <p key={idx}>{el}</p>;
          })}
        </div>
      </div>
    </div>
  );
}
// App.js
import { useState } from "react";
import useInput from "./util/useInput";
import Input from "./component/Input";
import "./styles.css";

export default function App() {
  const [fistValue, firstBind, firstReset] = useInput("");
  const [lastValue, lastBind, lastReset] = useInput("");
  const [nameArr, setNameArr] = useState([]);

  const handleSubmit = (e) => {
    e.preventDefault();
    setNameArr([...nameArr, `${fistValue} ${lastValue}`]);
    firstReset();
    lastReset();
  };

  return (
    <div className="App">
      <h1>Name List</h1>
      <div className="name-form">
        <form onSubmit={handleSubmit}>
          <Input labelText={"성"} value={firstBind} />
          <Input labelText={"이름"} value={lastBind} />
          <button>제출</button>
        </form>
      </div>
      <div className="name-list-wrap">
        <div className="name-list">
          {nameArr.map((el, idx) => {
            return <p key={idx}>{el}</p>;
          })}
        </div>
      </div>
    </div>
  );
}

// ./util/useInput.js
import { useState } from "react";

function useInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  const bind = {
    value,
    onChange: (e) => {
      setValue(e.target.value);
    }
  };

  const reset = () => {
    setValue(initialValue);
  };
  
//return 해야 하는 값은 배열 형태의 값
  return [value, bind, reset];
}

export default useInput;

// ./component/Input.js

function Input({ labelText, value }) {
  return (
    <div className="name-input">
      <label>{labelText}</label>
      <input {...value} type="text" />
    </div>
  );
}

export default Input;

3. React의 주목해야 할 기능

  • React 18 버전은 더이상 ReactDOM.render를 지원하지 않는다
const rootElement = document.getElementById("root");
ReactDOM.render(<AppTest />, rootElement);
  • React 18에서는 이제 createRoot API를 사용
import { createRoot } from "react-dom/client";

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

root.render(
    <App />
);

학습목표

  • 코드 분할(code spliting)에 대해 학습합니다.
  • React는 어떻게 코드 분할을 하는지 학습합니다.
  • React.lazy() 메서드의 쓰임새와 작성 방법에 대해 학습합니다.
  • suspense의 쓰임새와 작성 방법에 대해 학습합니다.

3-1. 코드 분할(Code Spliting)

  • 대부분 React 앱들은 Webpack, Rollup과 같은 툴을 사용해 번들링(Bundling)=>HTML 웹 페이지에 JavaScript를 쉽게 추가=> 과거에는 JavaScript 코드는 최소한의 수준으로 작성돼서 무리가 없었음
  • 모던 웹으로 발전하면서 점점 DOM을 다루는 정도가 정교해지며 JavaScript 코드 자체가 방대해지고 무거워짐 =>번들링하게 되면 특정 지점에서 코드를 해석하고 실행하는 정도가 느려짐
  • 코드 분할 : 지금 당장 필요한 코드가 아니라면 따로 분리를 시키고, 나중에 필요할 때 불러와서 사용 => 페이지의 로딩 속도를 개선
  • 번들 분할 혹은 줄이는 법
    • 서드파티 라이브러리
      • 플러그인이나 라이브러리 또는 프레임워크 등이 존재하며, 이 라이브러리를 잘 사용하면 편하고 효율적인 개발을 할 수 있다.
      • 서드파티 라이브러리는 사용자에게 다양한 메소드를 제공하기 때문에 코드의 양이 많고, 번들링 시 많은 공간을 차지
      • 따라서 사용 중인 라이브러리의 전부를 불러와서 사용하는 것보다 따로 따로 불러와서 사용할 수 있다면 많은 공간을 차지하지 않을 수 있게 된다.
/* 이렇게 lodash 라이브러리를 전체를 불러와서 그 안에 들은 메소드를 꺼내 쓰는 것은 비효율적입니다.*/
import _ from 'lodash';

...

_.find([]);

/* 이렇게 lodash의 메소드 중 하나를 불러와 쓰는 것이 앱의 성능에 더 좋습니다.*/
import find from 'lodash/find';

find([]);
  • React에서의 코드 분할
    • React는 SPA(Single-Page-Application)인데, 사용하지 않는 모든 컴포넌트까지 한 번에 불러오기 때문에 첫 화면이 렌더링 될때까지의 시간이 오래걸린다.
      => 사용하지 않는 컴포넌트는 나중에 불러오기 위해 코드 분할 개념을 도입
    • static import(정적 불러오기)
      • 코드 파일의 가장 최상위에서 import 지시자를 사용해 사용하고자 하는 라이브러리 및 파일을 불러오는 방법을 사용
    • React에서의 코드 분할 방법 : dynamic import(동적 불러오기)를 사용 => 구문 분석 및 컴파일해야 하는 스크립트의 양을 최소화
//Static Import
/* 기존에는 파일의 최상위에서 import 지시자를 이용해 라이브러리 및 파일을 불러왔습니다. */
import moduleA from "library";

form.addEventListener("submit", e => {
  e.preventDefault();
  someFunction();
});

const someFunction = () => {
  /* 그리고 코드 중간에서 불러온 파일을 사용했습니다. */
}

//Dynamic Import
form.addEventListener("submit", e => {
  e.preventDefault();
	/* 동적 불러오기는 이런 식으로 코드의 중간에 불러올 수 있게 됩니다. */

  import('library.moduleA')
    .then(module => module.default)
    .then(someFunction())
    .catch(handleError());
});

const someFunction = () => {
    /* moduleA를 여기서 사용합니다. */
}
  • Dynamic Import
    • dynamic import를 사용하게 되면 불러온 moduleA 가 다른 곳에서 사용되지 않는 경우, 사용자가 form을 통해 양식을 제출한 경우에만 가져오도록 할 수 있다.
    • dynamic import는 then 함수를 사용해 필요한 코드만 가져온다.
    • 가져온 코드에 대한 모든 호출은 해당 함수 내부에 있어야 한다.
    • dynamic import는 React.lazy 와 함께 사용할 수 있다.

3-2. React.lazy()와 Suspense

  • React.lazy()
    • React.lazy 함수를 사용하면 dynamic import를 사용해 컴포넌트를 렌더링할 수 있다. => 초기 렌더링 지연시간을 어느정도 줄일 수 있다.
    • React.lazy로 감싼 컴포넌트는 단독으로 쓰일 수는 없고, React.suspense 컴포넌트의 하위에서 렌더링을 해야 한다.
import Component from './Component';

/* React.lazy로 dynamic import를 감쌉니다. */
const Component = React.lazy(() => import('./Component'));
  • React.Suspense
    • Router로 분기가 나누어진 컴포넌트들을 위 코드처럼 lazy를 통해 import하면 해당 path로 이동할때 컴포넌트를 불러오게 되는데 이 과정에서 로딩하는 시간이 생기게 된다.
      =>Suspense는 아직 렌더링이 준비되지 않은 컴포넌트가 있을 때 로딩 화면을 보여주고, 로딩이 완료되면 렌더링이 준비된 컴포넌트를 보여주는 기능
    • Supense 컴포넌트의 fallback prop은 컴포넌트가 로드될 때까지 기다리는 동안 로딩 화면으로 보여줄 React 엘리먼트를 받아들인다.
    • Suspense 컴포넌트 하나로 여러 개의 lazy 컴포넌트를 보여줄 수도 있다.
/* suspense 기능을 사용하기 위해서는 import 해와야 합니다. */
import { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

function MyComponent() {
  return (
    <div>
			{/* 이런 식으로 React.lazy로 감싼 컴포넌트를 Suspense 컴포넌트의 하위에 렌더링합니다. */}
      <Suspense fallback={<div>Loading...</div>}>
				{/* Suspense 컴포넌트 하위에 여러 개의 lazy 컴포넌트를 렌더링시킬 수 있습니다. */}
        <OtherComponent />
				<AnotherComponent />
      </Suspense>
    </div>
  );
}
  • React.lazy와 Suspense의 적용
    • 앱에 코드 분할을 도입할 곳을 결정하는 것은 사실 까다롭기 때문에, 중간에 적용시키는 것보다는 웹 페이지를 불러오고 진입하는 단계인 Route에 이 두 기능을 적용시키는 것이 좋다.
    • 라우터에 uspense 적용
      • 라우터가 분기되는 컴포넌트에서 각 컴포넌트에 eact.lazy 사용하여 mport
      • Route 컴포넌트들을 uspense 감싼 후 로딩 화면으로 사용할 컴포넌트를 fallback 속성으로 설정
    • 초기 렌더링 시간이 줄어드는 분명한 장점이 있으나 페이지를 이동하는 과정마다 로딩 화면이 보여지기 때문에 서비스에 따라서 적용 여부를 결정해야 한다.
import { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  </Router>
);
profile
함께 일하는 프론트엔드 개발자 이성은입니다🐥
post-custom-banner

0개의 댓글