TIL

241119 TIL

GoJay 2024. 11. 20. 01:57
  • Udemy <한 입 크기로 잘라먹는 리액트>
    • Props Drilling이 발생하면 유지 보수도 어렵고, 디버깅도 어려울 수 있다. 이런 상황을 위해 리액트는 상태를 좀 더 쉽게 관리할 수 있도록 createContext 메서드를 제공한다.
    • createContext는 컴포넌트들이 상태를 등록하고 사용할 수 있는 Context를 제공한다. Context는 애플리케이션에서 전역적으로 접근하고 싶은 데이터를 저장할 수 있는 공간이다.
    • createContext는 보통 컴포넌트 외부에서 정의하고 호출한다. 컴포넌트 리렌더링 시 새로운 Context를 계속 다시 만들면 비효율이 발생하고, 관리되는 상태에 문제가 생길 수 있기 때문이다.
    • createContext()의 결과로 생성된 컨텍스트 객체에는 providerconsumer라는 메서드가 존재한다. 상위 컴포넌트에서 컨텍스트 값을 지정하기 위해서 provider를 사용하고, 하위 컴포넌트에서 읽기 위해서 consumer를 사용한다. consumer는 '대안적으로 드물게 사용되는' 방식이고(useContext 등장 전까지 사용되던 방법이고, 현재는 권장되지 않는다), 실제론 useContext(someContext)를 일반적으로 사용한다. useContext는 컴포넌트에서 컨텍스트를 읽고 구독할 수 있도록 해준 리액트 훅이다.
    • createContext()로 생성한 컨텍스트 객체는 컴포넌트로 사용될 수 있다. 예를 들어 const ItemContext = createContext()라고 선언-할당 되었으면, <ItemContext.Provider />라고 컴포넌트처럼 사용할 수 있다.
    • <ItemContext.Provider />와 같이 컨텍스트를 나타내는 컴포넌트 하위에는 데이터를 공유하려는 컴포넌트들을 위치시킨다. 그리고, value라는 속성에 값으로 컨텍스트에 포함시키고 싶은 상태를 정의한다. 아래와 같은 방식이다.
    return (
      <>
        <ItemContext.Provider value={{ todos, createItem, updateItem, deleteItem }}>
          <Header date={date}></Header>
          <Lists></Lists>
        </ItemContext.Provider>
      </>
    );
    • 컨텍스트를 사용해야 하는 위치에선 useContext의 인자로 생성한 컨텍스트 객체(ItemContext)를 전달하면 된다. 만약에 컨텍스트 객체를 생성한 파일 모듈과 컴포넌트 모듈이 분리되어 있다면 export-import 한 후 사용해야 한다. 이렇게 useContext의 인자로 컨텍스트 객체를 전달하면 useContext를 호출한 컴포넌트에서 컨텍스트를 읽을 수 있다.
    • useContext는 항상 호출하는 컴포넌트의 상위에서 가장 가까운 컴포넌트 객체.Provider를 찾는다. useContext를 호출하는 컴포넌트 안의 provider는 고려하지 않는다.
    • createContext로 생성된 컨텍스트는 관리하는 상태가 변경될 시 해당 상태를 구독한 컴포넌트들을 전부 자동으로 리렌더링 해준다.
    • createContext로 컨텍스트 객체를 생성한 뒤 구독하도록 하는 방식에서 useMemo, useCallback, memo 등 훅이나 리액트 내장 메서드를 사용할 경우 전달되는 참조형 데이터 상태의 참조가 변경되는지 아닌지를 잘 확인해야 한다. 만약에 참조가 변경되지 않도록 최적화 처리를 한 객체-배열-함수인데 다른 상태들과 같은 컨텍스트에 묶여서 리렌더링이 발생하게 된다면 컨텍스트를 두 개로 분리해서 관리하는 걸 고려하는 게 필요하다.
    • 리액트 SPA 애플리케이션 페이지 라우팅에 많이 사용되는 도구로 react-router가 있다. react-router를 사용하면 손쉽게 리액트 페이지를 라우팅 처리할 수 있다.
    • react-router를 사용하려면 main.jsxreact-router-dom에서 { BrowserRouter }를 불러와 아래와 같이 <App /> 컴포넌트를 감싸주면 된다.
    import { createRoot } from 'react-dom/client';
    import App from './App.jsx';
    import { BrowserRouter } from 'react-router-dom';
    
    createRoot(document.getElementById('root')).render(
      <BrowserRouter>
        <App />
      </BrowserRouter>
    );
    • BrowserRouterroot 노드 하위에 있는 모든 컴포넌트들에게 page path에 대한 정보를 전달한다. 이때, 내부적으론 <Navigation.Provider /><Location.Provider />를 통해 경로 관련 정보가 전달된다.
    • App 컴포넌트에선 react-router-domRoutersRouter 컴포넌트를 불러와 사용해 주면 된다. RoutersRouter 컴포넌트들을 감싸주는 부모 역할을 하고, Route 컴포넌트는 접속하는 URL 경로에 따라 보여줄 페이지를 설정해 주는 데 사용한다.
    • Route 컴포넌트의 path에는 접속 경로를, element에는 해당 경로로 접속했을 때 보여줄 페이지 컴포넌트를 전달한다. 아래와 같은 형식으로 사용한다.
    // ... 필요한 것들 import
    import { Routes, Route } from 'react-router-dom';
    
    function App() {
      return (
        <Routes>
          <Route path='/' element={<Home />}></Route>
          <Route path='/new' element={<New />}></Route>
          <Route path='/diary' element={<Diary />}></Route>
          <Route path='*' element={<NotFound />}></Route>
        </Routes>
      );
    }
    • 잘못된 경로로 들어왔을 땐 에러 페이지를 내려줘야 하는데, 이땐 path*를 입력해준다. 그러면 위에 지정해 준 경로 이외의 모든 경로에 대해 <NotFound /> 컴포넌트를 렌더링해 줄 수 있다.
    • react-router-dom을 사용해서 페이지를 이동하는 기능을 만들 땐 Link 컴포넌트와 useNavigate 훅을 사용할 수 있다. Link는 HTML의 <a> 태그와 유사하고, useNavigate는 경로를 이동시켜주는 Navigate 함수를 반환해 준다.
    • 아래와 같이 작성하면 <Link> 컴포넌트를 클릭했을 땐 to에 설정해 준 경로로, onClickButton 함수가 이벤트 핸들러로 등록된 button을 클릭하면 nav 네비게이터 함수에 설정한 경로로 이동할 수 있다.
    • 리액트에서 동적 라우팅을 구현하는 건 URL Parameter, Query Parameter 두 가지 방식이 있다. URL Parameter는 /diary/1과 같이 pathname 뒤에 /로 구분하고 이동할 경로를 추가해 주는 방식이다. 해당 방식을 구현하기 위해선 <Route> 컴포넌트의 path 뒤에 접근할 URL Parameter 정보를 :id와 같이 전달해줘야 한다(id는 임의로 넣어준 값이다. : 뒤의 값은 이후에 useParam으로 생성한 객체의 프로퍼티가 된다).
    // Diary 페이지에 동적 URL Parameter 설정
    // 설정 이후부턴 그냥 `/diary` 경로론 접근이 어렵고, 뒤에 id 정보를 붙여줘야 유효한 경로가 됨
    <Route path='/diary/:id' element={<Diary />}></Route>
    • URL Parameter로 설정한 값은 useParam 훅을 사용해 값을 받을 수 있음. 위와 같이 /diary/:id로 경로를 설정했으면, const param = useParam();으로 설정한 param 객체의 param.id로 URL Parameter에 접근할 수 있다.
    • Query Parameter는 Route 컴포넌트의 path에는 별도의 처리를 하지 않아도 되고, 경로에 대한 정보를 사용하는 곳에서 useSearchParam 훅을 사용하면 path 정보에 접근할 수 있다.
    • 사용 방식은 useState와 흡사하다. params은 쿼리 파라미터의 상태를 나타내고, setParams는 쿼리 파라미터의 상태를 변경하는 호출 메서드를 의미한다.
    import { useSearchParam } from 
    
    import { useSearchParams } from 'react-router-dom';
    
    const Home = () => {
      const [params, setParams] = useSearchParams();
      console.log(params.get('value')); // ?value={value}에 들어가는 value 값을 캐치
    
      return <>Home</>;
    };
    
    export default Home;
  • 리액트 Virtual DOM 알아보기
    • Virtual DOM은 리액트 공식 문서에 정식으로 사용하고 있는 용어는 아니다.
    • 리액트는 DOM의 업데이트를 경제적으로 하기 위해 전체 DOM 요소 중 변경이 발생한 부분만 선택적으로 업데이트한다.
    • 이러한 로직을 구현하기 위해선 상태 변경이 일어나기 전 기존의 DOM 요소에 대한 전체 정보가 필요할 것이고, 상태 변경이 일어났을 때 어디서 어떤 변화가 일어났는지를 파악해서 변경 사항을 반영한 DOM 요소에 대한 전체 정보들이 필요할 것이다.
    • 이러한 정보들이 메모리 상에서 자바스크립트 객체로 저장되어서 관리되고, 이러한 관리 방식이나 패턴을 흔히 Virtual DOM이라고 부른다.
    • 이런 맥락 때문에 Virtual DOM을 실제 DOM을 메모리 상에 복제한 복제본이라고 부르기도 하는 것 같은데, 리액트 공식 문서에서는 '패턴' 정도로 정의하고 있는 것 같다.
    • Virtual DOM은 동시에 두 개(또는 그 이상)를 메모리에서 관리하며, 상태 변경 이전-이후의 정보를 비교해서 변경 사항이 있는 부분을 파악한다. 이때, 두 개의 Virtual DOM을 비교하는 과정을 Diffing이라고 하며, 비교를 통해 확인한 변경 사항을 반영하는 것을 Reconcilation(재조정)이라고 한다.
    • 해당 과정을 통해 실제 변경이 필요한 DOM 요소만 실제 DOM에 업데이트해주며, 전체 DOM을 전부 다시 그리는 것이 아니기 때문에 상대적으로 경제적이다.
    • 리액트의 Diffing 알고리즘은 '휴리스틱 알고리즘'이다. 이는 100% 확신을 갖는 정답을 찾는 게 아니라, 합리적인 수준에서 괜찮다고 생각되는 답을 찾는 알고리즘을 뜻한다(컴퓨터 과학 용어인 것 같다). 그리고, 리액트에서 휴리스틱 알고리즘을 사용하기 위한 두 가지 전제가 있다.
      • 이전 트리와 새로운 트리에서 타입이 같은 노드끼리만 비교하고, 타입이 다르면 변경된 요소로 처리하고 그 하위 요소들을 리렌더 하는 대상에 포함시킨다.
      • 현제 요소 중 타입이 같은 요소는 key로 구분하며, key가 같은 요소들끼리만 변경 여부를 비교한다.
    • Diffing 알고리즘은 또한 Fiber Architecture 위에서 동작한다. Fiber Architecture는 Diffing에만 관련이 있는 게 아니라, 상태 변경을 추적하여 DOM을 리렌더링 하는 전체 과정에 적용되는 방법론이다.
    • Fiber Architecture는 리렌더링에 필요한 모든 작업을 작은 작업 단위(Fiber라고 부른다)로 나눠놓고, 작은 작업 단위들을 '사용자 경험에 결정적인 영향을 주는 것들'을 기준으로 우선순위를 결정하여, 우선순위대로 작업을 처리시킨다.
    • Fiber Architecture를 적용한 이후 리액트는 '동시성 처리'가 어느 정도 가능해졌다. 렌더링 과정이 완료되지 않더라도 사용자가 중간에 입력 값을 주거나(마우스-키보드 동작 등), 또는 사용자가 보는 화면의 애니메이션 요소를 미리 처리해줘야 한다면, 해당 작업은 다른 덜 중요한 작업이 완료되기 전에도 처리가 미리 가능하다.
    • Fiber Architecture 동작 분석에 대한 많은 자료들이 있는 것 같은데, 아직 잘 이해가 되진 않는다. 하나씩 차근차근 공부해 봐야겠다.