TIL
241119 TIL
GoJay
2024. 11. 20. 01:57
- Udemy <한 입 크기로 잘라먹는 리액트>
- Props Drilling이 발생하면 유지 보수도 어렵고, 디버깅도 어려울 수 있다. 이런 상황을 위해 리액트는 상태를 좀 더 쉽게 관리할 수 있도록
createContext
메서드를 제공한다. createContext
는 컴포넌트들이 상태를 등록하고 사용할 수 있는 Context를 제공한다. Context는 애플리케이션에서 전역적으로 접근하고 싶은 데이터를 저장할 수 있는 공간이다.createContext
는 보통 컴포넌트 외부에서 정의하고 호출한다. 컴포넌트 리렌더링 시 새로운 Context를 계속 다시 만들면 비효율이 발생하고, 관리되는 상태에 문제가 생길 수 있기 때문이다.createContext()
의 결과로 생성된 컨텍스트 객체에는provider
와consumer
라는 메서드가 존재한다. 상위 컴포넌트에서 컨텍스트 값을 지정하기 위해서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.jsx
에react-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> );
BrowserRouter
는root
노드 하위에 있는 모든 컴포넌트들에게 page path에 대한 정보를 전달한다. 이때, 내부적으론<Navigation.Provider />
와<Location.Provider />
를 통해 경로 관련 정보가 전달된다.App
컴포넌트에선react-router-dom
의Routers
와Router
컴포넌트를 불러와 사용해 주면 된다.Routers
는Router
컴포넌트들을 감싸주는 부모 역할을 하고,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;
- Props Drilling이 발생하면 유지 보수도 어렵고, 디버깅도 어려울 수 있다. 이런 상황을 위해 리액트는 상태를 좀 더 쉽게 관리할 수 있도록
- 리액트 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 동작 분석에 대한 많은 자료들이 있는 것 같은데, 아직 잘 이해가 되진 않는다. 하나씩 차근차근 공부해 봐야겠다.