-
리액트(React) 19버전 업데이트 전! 18버전 훑어보기!Front-end/React 2024. 5. 21. 10:04반응형
React(리액트)는 NextJS와 더불어 최고의 주가를 달리고 있는 프론트엔드 라이브러리이다.그러나 우리는 프론트엔드, 백엔드 등의 웹/앱 개발자이지 리액트 개발자가 아니다.
리액트 자체에 너무 한정적으로 공부할 필요는 없다.
그러나 리액트를 사용하겠다라고 하면 조금 더 깊이 있게, 혹은 명확히 알고 사용하는 것이 좋다고 생각한다.
그래서 ReactJS를 사용하는데 있어서 필요한 것들을 나누려고 한다.
ReactJS란 무엇인가?
다들 리액트 공식 문서는 한 번 이상 보면서 사용하고 있을 것이라고 생각한다.
그러나 보통 learn, 혹은 reference, api 쪽을 주로 보게 되는 것 같다.
그 라이브러리가 무엇인지 알기 위해서는 메인 홈페이지를 먼저 살펴보는 것이 좋다.
리액트에 대한 설명을 보면 Create user interfaces from components 즉, 컴포넌트를 이용해서 UI를 만드는 라이브러리이다.
썸네일, 좋아요 버튼, 비디오 등 각각을 개별적인 컴포넌트로 나누어서 관리할 수 있도록 만들고, 이것들을 합쳐서 스크린, 페이지, 앱을 만들게 되는 것이다.
짧게 말해, UI를 만드는 라이브러리이다.
ReactJS는 어떻게 동작하는가?
리액트가 무엇인지 알았다면 어떻게 동작하는지도 알아보자.
React 애플리케이션이 실행되면 Virtual DOM(가상 돔)이 메모리에 생성 된다.
Virtual DOM은 Real DOM(실제 돔)의 구조만 간결하게 흉내를 낸 Javascript 객체이며, Real DOM의 속성(class 등)은 가지고 있지만 Real DOM이 갖고 있는 API는 갖고 있지 않다.
그래서 실제 DOM 요소의 모든 속성과 특성을 포함하는 경량화 된 Javascript 객체이며, HTML의 추상화 버전이라고 불린다.
React가 리렌더링 되는 경우는 주로 state나 props가 변경될 때이다.
state나 props가 변경이 되면, React는 새로운 Virtual DOM 트리를 만들게 되고, React의 Reconciliation과 결합 된 Virtual DOM은 Diffing 알고리즘을 통해서 이전의 Virtual DOM과 차이를 계산한다.
그런 다음 변경 된 부분만 Real DOM에 업데이트하여 전체 페이지 새로고침의 필요성을 최소화 하고 성능을 향상 시키게 된다.
Diffing Algorithm
- Element의 속성 값만 변경 된 경우 : 속성 값만 업데이트 시킨다
- Element의 tag 또는 component가 변경 된 경우 : 해당 노드를 포함한 하위의 모든 노드를 unmount 즉, 제거하고 새로운 Virtual DOM으로 대체한다
- 이렇게 모든 변경이 이루어진 후에는 딱 한 번 Real DOM에 결과를 업데이트한다Element와 Component의 차이는 무엇인가?
React의 Element는 Component Instance 또는 DOM node와 propeties(props)를 설명하는 일반 객체(plain object) 이다.
Element는 리액트의 가장 작은 구성요소 이며, 일반적으로 Javascript의 구문 확장자인 jsx로 생성된다.
const foo = React.createElement("div"); // 아래와 같이 만들어지게 될 것이다 { type: "div", props: {} }
React의 Component는 하나 이상의 요소로 구성 될 수 있는 재사용 가능한 UI를 말하며 이는 Class component일 수도 있고, Function component로 사용할 수도 있다.
const App(){ return <div>Hello World</div>; } export default App;
왜 state를 직접 업데이트하면 안될까?
리액트를 사용하면서 가장 많이 사용하게 되는 훅이 아마 useState 일 것이라고 생각한다.
그런데 useState를 살펴보면 항상 두 가지가 같이 사용된다 바로 [state, setState] 이다.
const [state, setState] = useState();
바로 state에 할당하면 되지 왜 setState를 통해서 업데이트해야 할까?
이는 React의 Rendering 동작 방식과 관련이 있다.
React는 state의 변경을 감지해서 컴포넌트를 다시 렌더링하는데, 이 과정에서 상태를 직접 수정하게 되면 React는 상태변경을 감지하지 못할 수 있고 이는 사이드 이펙트를 초래할 수 있으며, 상태관리를 어렵게 만들 수 있다.
UI에 업데이트 된 상태가 반영되지 않아 불일치가 발생하고 디버그가 어려운 버그가 발생할 수 있는 것이다.그렇기 때문에 React에서는 setState를 제공함으로써 상태를 업데이트 할 때 다시 렌더링 하도록 만드는 것이다.
또한, setState 함수를 통해서 상태를 업데이트할 때 React는 상태를 비동기적으로 처리하며, 여러 개의 setState 호출을 하나로 묶어서 처리할 수 있다.
이를 통해 성능 최적화가 가능하고, 여러 상태 업데이트가 동시에 이루어져도 사이드 이펙트가 발생하지 않도록 할 수 있다.
setState 콜백 함수의 목적은 무엇일까?
위에서 설명한 것처럼 setState()는 비동기 작업을 수행한다.
그래서 React를 처음 공부할 때 볼 수 있는 잦은 실수 중 하나가 바로 하나의 setState()를 통해서 한 번에 여러 작업을 할 때이다.
만약 +3을 시킬 수 있는 작업을 한다고 해보자.
const [count, setCount] = useState(0); const CountHandler = () => { setCount(count + 1); setCount(count + 1); setCount(count + 1); }
만약 위와 같이 로직을 만들고 CountHandler를 작동 시켰을 때, count가 3이 되기를 원한다면 생각처럼되지 않을 것이다.
위의 결과는 3이 아닌 1이 되기 때문이다.
그렇다면 어떻게 해야 3이 나오도록 할 수 있을까?
정답은 아래와 같다.
const [count, setCount] = useState(0); const CountHandler = () => { setCount((prev) => prev + 1); setCount((prev) => prev + 1); setCount((prev) => prev + 1); }
이렇게 해야 CountHandler를 실행 할 때마다 count가 3으로 업데이트가 된다.
그 이유는 setState는 비동기적으로 작동하고 즉시 변경되지 않기 떄문이다.
그래서 this.state()를 호출하게 되면 잠재적으로 기존 값이 반환 될 수 있다. 다시 말해, setState를 사용할 때 현재 state에 의존해서는 안된다는 의미이다.
이를 해결하기 위해, 이전 상태의 인수(prev)를 사용해서 setState 함수에 전달하는 것이다. 그러면 비동기 특성으로 인해서 사용자가 엑세스 할 때 이전 상태 값을 가져오는 문제를 피할 수 있다.
forwardRef는 무엇인가?
useRef는 리액트 초심자도 사용해본 경험이 있었는데, 생각보다 forwardRef에 대해서 잘 모르는 경우를 봤다.
물론 최근에 소개 된 React 19버전부터는 이 또한 신경쓰지 않아도 되는 것으로 보이지만 우선 forwardRef를 알아두자면 이 기능을 통해서 부모 컴포넌트가 자식 컴포넌트의 DOM 요소나 인스턴스에 직접 접근할 수 있다.
즉, 아래와 같은 상황에서 주로 유용하게 사용할 수 있다.
- 자식 컴포넌트의 DOM 요소에 접근할 필요가 있을 때
- 외부 라이브러리와 통합할 때
- 특정 동작을 자식 컴포넌트에서 처리해야 할 때
...사용 예시를 보자면 아래와 같다.
import React, { useRef, forwardRef } from 'react'; const ParentComponent = () => { const inputRef = useRef(null); const focusInput = () => { if (inputRef.current) { inputRef.current.focus(); } }; return ( <div> <MyInput ref={inputRef} /> <button onClick={focusInput}>Focus Input</button> </div> ); }; const MyInput = forwardRef((props, ref) => ( <input {...props} ref={ref} /> ));
외부 라이브러리를 사용할 때도 위와 크게 다르지 않다.
import React, { forwardRef } from 'react'; import ExternalLib from '-- 어떤 외부 라이브러리 --'; const CustomInput = forwardRef((props, ref) => ( <ExternalLib {...props} inputRef={ref} /> )); export default CustomInput;
ref는 마치 map을 사용했을 때 key와 같다고 보면 된다.
map을 컴포넌트 안에서 사용할 때 key를 전달해주게 되는데, 이는 props로 넘겨지지 않는다는 것을 알고 있을 것이다.
ref 또한 마치 key처럼 전달이 되지 않기 때문에 forwardRef를 사용해서 컴포넌트 간의 ref 전달을 가능하게 할 수 있다.
React Fiber란 무엇인가?
이제 곧 19버전을 앞두고 있는 리액트이지만 현재 사용되고 있는 React Fiber에 대해서 알면 도움이 될 것 같다.
React Fiber는 React의 핵심 아키텍쳐 중 하나로 React 16버전에 도입된 재조정 알고리즘(Reconciliation algorithm)이다.
기존의 재조정 알고리즘은 "Stack Reconciler" 라고 불리며, 깊이 우선 탐색(DFS)을 통해서 한 번에 전체 컴포넌트 트리를 처리했다.
그러나 이는 동기적인 처리로 성능 문제가 발생할 수 있고 특히 복잡한 어플리케이션에서 끊김 현상 등 UI가 좋지 않은 문제가 발생했다.
Fiber는 이런 문제를 개선하기 위해서 도입되었다.
비동기적으로 만들고, 작업을 더 작은 단위로 나누고, 각 단위를 "Fiber"라는 데이터 구조로 관리하며 보다 효율적으로 재조정 작업을 수행하도록 하는 것이다.
React가 작업의 우선 순위를 지정하여 작업을 중단하고, 필요에 따라 나중에 다시 시작할 수 있도록 하여 메인 스레드가 차단되는 것을 방지할 수 있고 그에 따라 UI와 성능을 향상 시킨 것이다.
Controlled와 Uncontrolled는 무엇인가?
리액트에서 폼(form)을 처리하는 두 가지 주요 방법이 있는데 기본저긍로 각각은 기본적으로 데이터 관리 방법이 다르다.
비제어(Uncontrolled) 컴포넌트
DOM 자체에 폼 데이터를 저장하는 방식이다. 이 방식에서는 입력 값은 DOM에 의해 관리되며 폼 요소의 내부 상태를 직접 관리하지 않고, 폼을 제출할 때 등 필요로할 때 DOM에서 값을 직접 가져온다.
이를 위해 React의 useRef를 사용해 DOM 요소에 직접 접근하게 된다.
이는 초기 셋팅이 간단하고 간단한 폼이나 단일 입력 필드에 적합 할 수 있으며, 비동기적인 데이터 소스와의 통합이 용이할 수 있다.
그러나 폼의 상태를 직접 관리하므로 React외부에서 폼의 상태를 추적하기 어령루 수 있고, 유효성 검사와 같은 추가 로직 구현이 복잡 할 수 있다.
import React, { useRef } from 'react'; const UncontrolledComponent = () => { const inputRef = useRef(null); const handleSubmit = (event) => { event.preventDefault(); console.log('Input value:', inputRef.current.value); }; return ( <form onSubmit={handleSubmit}> <input type="text" ref={inputRef} /> <button type="submit">Submit</button> </form> ); };
제어(Controlled) 컴포넌트
비제어 컴포넌트와 반대로 DOM이 아니라 리액트에 의해서 값이 제어가 되는 경우를 말한다.
입력 값은 React 상태에 의해서 제어가 되며 입력에 대한 변경 사항은 상태를 업데이트하는 이벤트 핸들러를 통해서 처리된다.
이는 초기 코드량이 많아질 수 있으며 각 폼 요소에 대한 핸들러 함수를 작성해야하지만 state를 통한 일관된 방법으로 폼의 상태를 관리할 수 있고, 데이터 변화를 쉽게 추적하고 유효성 검사나 조건부 로직 처리가 용이하다.
import React , { useState } from 'react' ; const ControlledComponent = ( ) => { const [value, setValue] = useState ( '' ); const handlerChange = ( event ) => { setValue (event.target . value ) ; }; return ( <input type="text" value={value} onChange={handleChange} /> ); };
리액트의 에러 경계(Error boundaries)는 무엇인가?
에러 경계라고 불리는 Error boundaries는 마치 Javascript의 catch 블록처럼 작동하는데 컴포넌트에 대해서 작동시킬 수 있는 것이다.
우선 우리가 try ~ catch 문을 사용하는 이유 중 하나가 Exception을 해결하기 위해서이다.
리액트 또한 어떤 자식 컴포넌트에 Exception이 발생하게 되면 부모 컴포넌트까지 unmount 되면서 결국 사용자는 하얀 화면을 마주하게 된다.
만약 여러 컴포넌트로 구성 된 메인 화면이 있을 때, 어떤 하나의 컴포넌트에서 예외가 발생해도 전체가 다 unmount 되는 것이다.
그런데 만약 각각의 컴포넌트를 Error boundaries를 걸어준다면?
그러면 화면이 다 날아가는게 아니라 에러가 발생한 부분만 Exception이 나타날 것이고, 그 부분만 포착하여 새로고침을 하거나 문제를 해결해줄 수 있을 것이다.
클래서로만 에러 경계를 사용할 수 있는데, 그 구성은 아래와 같다.
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // 에러가 발생하면 다음 state를 반환해서 다음 렌더링에서 백업 UI를 보여줄 수 있게 한다. return { hasError: true }; } componentDidCatch(error, errorInfo) { // 에러 로깅 console.error("ErrorBoundary caught an error", error, errorInfo); } render() { if (this.state.hasError) { // 에러가 발생했을 때 렌더링할 백업 UI return <h1>Something went wrong.</h1>; } return this.props.children; } }
import React from 'react'; import ErrorBoundary from './ErrorBoundary'; import MyComponent from './MyComponent'; const App = () => ( <ErrorBoundary> <MyComponent /> </ErrorBoundary> ); export default App;
Error Boundaries는 static getDerivedStateFromError() 와 componentDidCatch() 라이프 사이클 메서드를 통해서 구현된다.
getDerivedStateFromError() 메서드는 에러가 발생했ㅇ르 때 백업 UI를 보여주기 위해서 컴포넌트의 state를 업데이트를 한다.
componentDidCatch() 메서드는 여러 정보를 로깅하는데 사용된다.
만약 클래스가 익숙하지 않거나 훅스로 사용하고 싶다면 라이브러리도 있으니 훨씬 간편하게 사용할 수 있을 것이다.
리액트 18버전에서의 새로운 기능은?
리액트 19버전을 앞두고 있는 지금, 많은 것이 업데이트 되겠지만 사실 개인적으로는 React 18버전에서 눈에 띌만한 많은 업데이트가 있었다고 생각된다. 그럼 그 때는 어떤 업데이트가 있었는지 알아보자.
Automatic batching (자동 일괄 처리)
React 18 버전에서는 상태 업데이트를 그룹화하여 한 번에 렌더링하는 자동 배치 처리 기능이 도입됐다.
이렇게 함으로써 갖는 이점은 DOM이 업데이트 되는 횟수를 줄일 수 있고 그로 인해 성능이 향상 될 수 있으며 불필요한 re-rendering을 피할 수 있는 것이다.
const [count, setCount] = useState ( 0 ); const [alert, setAlert] = useState ( false ); const handlerClick =( )=>{ setCount ((prev) => prev + 1 ); setAlert ((prev) => !prev ); // React는 마지막에 한 번 리렌더링 되는데 이것이 Batch이다 }
그러나 항상 그런 것은 아니다. 몇 가지 예외가 있는데 그 중 하나가 이벤트가 처리된 후 상태를 업데이트 할 때이다.
const [count, setCount] = useState(0); const [alert, setAlert] = useState(false); const handleClick =()=>{ fetch().then(() => { setCount((prev) => prev + 1); // React 리렌더링! setAlert((prev) => !prev); // React 리렌더링! }); }
Concurrent (동시성)
React 18버전부터는 React가 동시에 여러 작업ㅇ르 수행할 수 있는 새로운 동시성 모드도 도입되었다.
이는 유저의 input에 대해서 더 잘 반응하도록 하여 성능을 향상시킨 것이다.
이전 버전의 경우 React는 렌더링이 한 번 시작되면 완료될 때까지 중단할 수 없었던 문제가 있었다.
이는 좋지 않은 UX를 남기기도 했는데 18버전에서는 React가 다양한 작업의 중요성과 긴급성을 기반으로 업데이트 및 렌더링의 우선순위를 자체적으로 지정하여 우선순위가 높은 것부터 업데이트를 시켜 더 빠르게 처리 되도록 하는데 도움을 줄 수 있게 되었다.
사용자의 인터렉션(상호작용)에 신속하게 응답하기 위해서 렌더링을 중단, 일시중지, 재개 할 수 있게 되었고, 더 긴급한 작업 순서대로 우선순위로 일을 처리하게 되었다.
Suspense
React 18버전에서는 데이터를 사용할 수 있을 떄 까지 React가 컴포넌트 렌더링을 지연시킬 수 있는 Suspense 기능이 도입되었다.
이는 React가 데이터를 기다리는 동안 빈화면을 보여주느게 아니라 다른 대체 화면을 미리 보여줌으로써 UX를 높일 수 있게 되는 것이다.
Server component
React 18에서는 React가 서버에서 미리 렌더링하고 클라이언트로 스트리밍 할 수 있도록 하는 새로운 서버 구성 요소 기능도 도입되었다.
이를 통해서 클라이언트가 초기에 다운로드 해야하는 Javascript의 양이 줄어들어서 그만큼 빨리 부를 수 있게 되었고 그에 따라 성능이 향상 될 수 있었다.
Transitions
UI 변경 사항을 애니메이션화 할 수 있는 Transitions 또한 도입이 되었는데 이를 통해서 UI 변경 사항이 더욱 원활하게 표시되어서 UX 향상을 기대할 수 있다.
새로운 Strict mode
strict mode를 사용하면 내부 컴포넌트 트리에 대한 추가 개발 동작 및 경고를 활성화할 수 있다.
import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; const root = createRoot(document.getElementById('root')); root.render( <StrictMode> <App /> </StrictMode> );
Strickt Mode에서 개발을 하게 되면 아래 동작이 활성화 된다.
- 컴포넌트가 불완전한 렌더링에 의한 버그를 찾기 위해 추가 시간을 들여 리렌더링한다
- 컴포넌트가 누락 된 Effect cleanup에 의한 버그를 찾기 위해 시간을 들여 Effect를 재실행한다
- 컴포넌트가 사용되지 않는 API를 사용하고 있는지 체크한다
React 18에서의 strict mode는 컴포넌트가 mount, unmount를 반복해도 영향을 받지 않도록 했다.
예를 들어서, 사용자가 화면에서 탭을 내렸다가 다시 돌아오면 이전 화면이 즉시 표시되어야하는 것이다.
일반적인 흐름은 아래와 같다.
1. 사용자가 처음 화면에 접근하면 React는 컴포넌트를 Mount 시킨다.
2. 사용자가 화면에서 벗어나면 React는 컴포넌트를 Unmount 시킨다.
3. 사용자가 다시 화면으로 돌아오면 React는 컴포넌트를 다시 Mount 시킨다.
새로운 훅의 추가
React 18버전에서는 개발자의 생산성을 높이고, 더 나은 UX를 제공할 수 있도록 새로운 훅이 도입되었다.
useId()
기존에 Unique ID를 만들기 위해서 new Date() 함수를 사용하는 등 랜덤한 숫자를 만드는 경우가 있었을 것이다.
그러나 이제는 useId라는 것을 이용해서 고유 ID를 생성할 수 있다.
이는 SSR(서버 사이드 렌더링)에서도 동일한 ID를 보장해주므로 클라이언트와 서버간의 일관성을 유지하는데도 유용하고 특히 Form 요소나 ARIA 속성을 사용할 때 유용하다.
아래 예시처럼 작성하게 되면 각 컴포넌트 인스턴스 마다 고유한 ID가 생성되므로 접근성, 일관성을 유지하기 좋다.
import React, { useId } from 'react'; function MyComponent() { const id = useId(); return ( <div> <label htmlFor={id}>Name:</label> <input id={id} type="text" /> </div> ); }
useTransition()
User가 인터렉션하는 동안 state를 업데이트를 비동기적으로 처리하여 UI가 더욱 빠르고 부드럽게 렌더링 될 수 있도록 도와준다.
주로 UI 업데이트를 우선순위에 따라 처리하게 되고, 덜 중요한 업데이트를 배치하여 사용자 경험을 향상시킨다.
아래 예시처럼 작성하게 되면 startTransition을 사용함에 따라 입력이 일어날 때 비동기적으로 state를 업데이트하여 애플리케이션 응답성을 유지한다.
import React, { useState, useTransition } from 'react'; function App() { const [isPending, startTransition] = useTransition(); const [value, setValue] = useState(''); const handleChange = (e) => { startTransition(() => { setValue(e.target.value); }); }; return ( <div> <input type="text" onChange={handleChange} /> {isPending ? <p>Loading...</p> : <p>{value}</p>} </div> ); }
useDeferredValue()
값의 업데이트를 지연시켜서 성능 최적화에 도움을 주는 훅이다. 이는 주로 복잡한 계산이나 렌더링이 필요한 경우 유용하게 사용된다.
아래 예시를 보면 useDefferedValue를 사용하고 있는데 입력이 변경될 때마다 복잡한 검색 로직을 지연시켜서 성능을 향상시킬 수 있다.
import React, { useState, useDeferredValue } from 'react'; function SearchComponent() { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); const results = performComplexSearch(deferredQuery); return ( <div> <input type="text" onChange={(e) => setQuery(e.target.value)} /> <SearchResults results={results} /> </div> ); } function performComplexSearch(query) { // 복잡한 검색 로직 }
useSyncExternalStore()
이는 외부 스토어의 상태를 구독하고 동기화하는데 사용된다.
주로 외부 상태 관리 라이브러리(Mobx, redux, zustand 등)와 함께 사용할 수 있다.
아래 예시처럼 사용하게 되는 경우, 외부 스토어의 상태를 구독할 수 있고, 스토어가 업데이트 될 때 컴포넌트가 리렌더링 되도록 한다.
import { useSyncExternalStore } from 'react'; function useExternalStore(store) { return useSyncExternalStore( store.subscribe, store.getState ); } function MyComponent({ store }) { const state = useExternalStore(store); return <div>{state}</div>; }
useInsertionEffect()
이 훅은 DOM이 업데이트 되기 전에 실행된다.
style을 동기적으로 적용할 때 사용할 수 있는데 style을 동적으로 삽입하거나 DOM 변경 전에 필요한 작업ㅇ르 수행하는데 유용하다.
아래 예시처럼 사용할 경우 useInsertionEffect는 컴포넌트가 mount 되기 전에 스타일을 동기적으로 삽입하고, unmount 될 때 스타일을 제거하게 된다.
import React, { useInsertionEffect } from 'react'; function MyStyledComponent() { useInsertionEffect(() => { const style = document.createElement('style'); style.textContent = ` .my-class { color: red; } `; document.head.appendChild(style); return () => { document.head.removeChild(style); }; }, []); return <div className="my-class">Hello, world!</div>; }
이제 리액트 19 버전이 나오는데 앞서 18버전에서 업데이트 되었던 부분을 정리해보았다.
히스토리를 알고 앞으로 나오는 새로운 것들을 받아들여야 이유를 알게 되고, 무엇이, 왜 업데이트 되는지 알 수 있을 것이다.
그리고 리액트를 자주 사용하는 프론트엔드 개발자라면 이 정도는 알아두는 것이 도움이 될 것이라고 생각된다.
참고 : Top 40 ReactJS Interview를 기반으로 내용을 추가한 글입니다
반응형'Front-end > React' 카테고리의 다른 글
npm publish 해보기 ( react-step-slider ) (0) 2025.01.07 ReactJS의 디자인 패턴에 대해서 알아보자! (0) 2024.05.22 TanStack Table v8 - Merge header cell (헤더 병합) (0) 2024.03.07 LocalStorage로 저장, 불러오기, 삭제 (JS, React) (2) 2023.02.05 리액트 파일 업로드, 파일 다운로드 File upload, download with javascript react (2) 2022.06.24