React

프래그먼트 작업, wrapper로 감싸기, Portals, Ref

창고관리장 2023. 5. 17. 00:14

 

JSX코드의 제한사항을 해결하는 여러 방법

 

리액트를 사용하며 두 개 이상의 요소를 사용할 때는 JSX 코드의 제한사항에 의해 한 개의 요소만 반환해야 한다. 그래서 만약 한 컴포넌트 파일에 h2와 p 요소가 있다면 이 두 개를 하나의 div나 다른 요소 안에 넣어 반환을 하는 식으로 사용하게 된다.

 

이렇게 하는 것은 일반적인 해결방법이긴 하지만, 웹사이트 전체를 들여다봤을 때 수많은 div가 렌더링되는 모습을 볼 수 있다. 이를 div 수프라고 불리는데, 여러 div 중에 불필요한 요소가 있을 수 있기에 이를 해결해줄 필요가 있다.

 

이를 위해 helper라 명명한 폴더 내에 직접 받은 자식요소만 반환하는 Wrapper라는 파일을 만들어 활용할 수 있다.

감싸는 용도의 wrapper 파일 코드

이 파일은 사실상 아무것도 없는 파일이기에 다른 컴포넌트에서 여러 요소들을 감싸는 용도로 사용할 수 있다. 

wrapper의 활용

이렇게 하면 콘솔창의 변환된 HTML 코드에도 나올 듯 싶은데 전혀 나오지 않으며, Wrapper 내부의 요소들이 실행될 때만 해당 코드들이 표출된다. 어떻게 보면 불필요한 div 요소들을 삭제하면서 보이지는 않게 속임수를 쓴 셈이다.

 

이 wrapper를 직접 파일로 만들어 사용하는 방법은 흔하지 않다. 리액트에서 공식 지원하는 방법과 효과가 동일하기 때문이다. 

 

공식 지원하는 방법은 React 라이브러리 내의 Fragment를 사용하는 것이다. 이 프래그먼트는 import 구문에서 밝혀준다면 사용 시에 React.Fragment가 아닌 Fragment만 써서 사용할 수 있게 된다.

프래그먼트 사용 예시

프래그먼트는 위 이미지의 코드처럼 사용하게 된다. 만약 임포트 구문에서 밝혀주지 않더라도 요소들을 감쌀 부분에 <React.Fragment>를 입력하여 사용할 수 있다.

 

포탈과 Ref

 

포탈은 느낌 그대로 다른 곳으로 이동시키는 것이다. 오늘 만든 연습용 웹페이지에는 백드랍 요소가 포함된 오버레이 요소들을 만들었는데, 보통은 body 태그의 바로 아래에 위치하지만, 만들다보니 메인섹션 내의 몇개의 div 요소 안에 파묻혀 있었다. 이렇게 만들더라도 당장은 기능상 오류는 없지만, 내가 아닌 다른 개발자가 이 코드로 작업을 한다면 여러 애로사항이 생길 수 있는 여지가 있다.

 

그래서 이럴 경우 포탈을 사용하게 되는데, 이를 위해 우선 index.html 파일 내 body 바로 아래 부분에 aside_root라는 id를 가진 div 요소를 만들어주는 것으로 시작했다.

HTML 파일 내 오버레이창이 위치할 곳의 지정

그리고 오버레이 요소가 있는 파일로 가서 ReactDOM을 임포트 구문으로 불러오고, 이동할 부분에 ReactDOM.createPortal 메서드를 사용해주었다. 

 

💡 createPortal() 메서드: 리액트 컴포넌트 트리의 DOM 노드에 컴포넌트를 렌더링하는 데 사용되는 메서드이다. 일반적으로 리액트 컴포넌트는 부모 컴포넌트의 자식으로서 렌더링되지만, 이 메서드를 활용하면 리액트 컴포넌트를 다른 DOM 요소에 렌더링할 수 있다. 
이 메서드는 child와 container라는 두 인자를 사용한다. child에 들어갈 인자는 렌더링할 리액트 컴포넌트 또는 요소이며, container에 들어갈 인자는 컴포넌트가 렌더링될 DOM 요소를 뜻한다. 

 

createPortal 메서드로 이동시킬 부분 감싸기

오늘 연습한 웹페이지에서 오버레이 부분은 반환문에 모두 들어있어서 이를 통째로 createPortal의 첫 번째 인자로 지정해주었다. 두 번째 인자로는 오랜만에 getElementById 메서드를 이용해서 HTML 파일의 aside_root id를 지정해주었다.

 

이렇게 하면 이 부분이 렌더링 될 때, body 태그의 바로 다음에 있는 aside_root 요소에서 렌더링되게 된다.

정상적인 위치에서 렌더링된 오버레이 요소

위 이미지는 콘솔창에서 오버레이 부분이 정상적으로 렌더링되는지 확인하는 모습이다. 

 

오늘은 Ref에 대해서도 봤는데, 사용자가 입력한 데이터를 받아서 저장하기 위해 state 값을 사용하는데, 이 경우 입력할 때마다 상태를 업데이트하는 것은 조금 과할 수 있다. 이 경우에 ref를 사용한다.

ref를 사용하기 위한 준비

ref를 사용하기 위해 위 코드처럼 임포트 구문에 useRef를 추가해주었고, 메인 함수 안에 이름과 나이를 담을 새로운 상수를 만들어 값을 useRef로 하며 기본값은 undefined로 해주었다. 

 

💡 useRef 훅: useRef 훅은 컴포넌트 내에서 DOM 요소에 접근하기 위해 사용되는 훅이다. 이를 이용하면 컴포넌트의 생명주기와 독립적으로 DOM 요소를 참조하고 조작할 수 있다. useRef를 사용하여 DOM 요소에 접근하는 방법은 우선 ref 변수를 선언하고, ref 변수를 원하는 DOM 요소의 ref 속성에 할당하며, ref.current를 통해 ref 변수에 할당된 DOM 요소에 접근할 수 있다.
이 useRef는 DOM 요소에 접근하여 값을 읽거나 수정할 수 있으며, 외부 라이브러리나 API와 상호작용하기 위해 해당 요소에 대한 참조를 유지할 수 있으며, 컴포넌트 내에서 이전 값과 현재 값의 차이를 추적할 수도 있다.
또한, useRef를 사용하면 컴포넌트의 상태가 변경되어 다시 렌더링되어도 ref 변수는 이전 값과 동일한 값을 유지하게 된다. 이는 useRef를 통해 DOM 요소에 대한 일관된 접근을 유지할 수 있도록 해준다.

ref로 입력 값을 받기 위한 코드 수정

그 아래에서는 ref로 입력 값을 받기 위한 작업을 했다. 우선 form에 연결된 submitHandler 함수에 입력 값인 nameRef.current.value와 ageRef.current.value를 각각의 상수에 저장해주었고, 값들이 저장되는 userInfo 객체에도 name과 age를 이 상수명으로 수정해주었다.

 

또한, 조건문에서도 입력값이 저장된 상수명으로 모두 수정했고, 입력 값을 받아 리스트에 표출되면 입력한 값이 초기화되도록 하기 위해 빈 문자열로 다시 지정해주었다. (이 가운데 코드에서 ref를 사용하는 것은 리액트를 사용하는 것이 아니라는 것이 확인된다.)

 

form 내의 input 요소에도 기존의 state를 활용한 value 속성과 onChange 이벤트리스너를 삭제해주었다. 그 대신 ref 속성을 추가해서 새 상수인 nameRef와 ageRef를 값으로 지정해주었다.

 

또한, 이 파일 내에서 state를 활용한 모든 코드들을 삭제해주었다. 더 이상 state를 활용하지 않게 되었기 때문이다.

 

이 ref를 사용하는 것은 '제어되지 않는 컴포넌트'라고 불리는데, 그 이유는 ref를 통해 직접 DOM 요소에 접근하여 조작하는 것이기 때문이다. 일반적으로 리액트에서는 컴포넌트의 state와 속성(props)를 통해 컴포넌트를 제어하고 관리하는 방식을 선호한다. 이를 '제어되는 컴포넌트'라고 부른다.

 

하지만 ref를 사용하면 컴포넌트의 상태나 속성을 통하지 않고 직접 DOM 요소에 접근하여 조작할 수 있다. 이는 컴포넌트의 상태와 동기화되지 않은 변경이 발생할 수 있고, 리액트의 가상 DOM과 조화되지 않는 동작을 수행할 수 있다. 따라서 ref를 사용하는 경우에는 주의가 필요하며, 리액트의 가이드라인에서는 가능한 ref 사용을 피하고 state와 props를 활용하여 컴포넌트를 제어하는 방식을 권장하고 있다.

 

이 '제어되지 않는 컴포넌트' 같은 경우, 외부 라이브러리나 웹 플랫폼과의 통합이 필요한 경우에 활용될 수 있다. 

 

💡 리액트의 가상 DOM: 이는 리액트에서 UI를 관리하기 위해 사용되는 가상의 메모리 내에 존재하는 가상화된 복제본을 말한다. 이 가상 DOM은 실제 DOM과 유사한 구조를 가지며, 리액트 컴포넌트의 계층 구조와 상태를 표현한다. 가상 DOM은 리액트에서 성능을 최적화하고 효율적인 UI 업데이트를 가능하게 한다. 일반적으로 웹 어플리케이션에서 UI 업데이트는 실제 DOM을 직접 조작하여 변경사항을 반영하게 된다. 그러나 실제 DOM 조작은 비용이 많이 들고, 브라우저의 리플로우와 리페인트같은 비용이 큰 연산을 유발할 수 있다.
리액트는 가상 DOM을 사용하여 이런 문제를 해결한다. 가상 DOM은 실제 DOM과는 독립적으로 존재하며, 리액트 컴포넌트의 상태 변화에 따라 가상 DOM이 업데이트된다. 그리고 가상 DOM의 업데이트는 실제 DOM과 비교하여 변경된 부분만을 실제 DOM에 적용하게 된다. 이를 통해 실제 DOM 조작을 최소화하고 효율적인 UI 업데이트를 수행할 수 있다.
그래서 가상 DOM은 리액트의 핵심 개념 중 하나이며, 리액트가 성능과 효율성을 제공하는 핵심 메커니즘에 해당한다. 
💡 브라우저의 리플로우(reflow)와 리페인트(repaint): 웹페이지에서 UI를 업데이트할 때 발생하는 브라우저의 연산 과정을 뜻한다. 리플로우는 요소의 크기, 위치, 레이아웃 등의 변경이 있을 때 발생하는 과정이다. 리플로우는 해당 요소뿐만 아니라 그와 연관된 다른 요소들에 대해서도 다시 계산하여 레이아웃을 재구성하게 된다. 따라서 리플로우는 비용이 많이 드는 연산이며, 페이지의 다른 요소들에도 영향을 미칠 수 있다. 
리페인트는 요소의 스타일, 색상 등의 시각적인 변경이 있을 때 발생하는 과정이다. 리페인트는 실제로 화면에 그려지는 픽셀들의 색상을 업데이트하는 과정을 수행하는데, 리플로우와는 달리 레이아웃을 다시 계산할 필요가 없으므로 비교적 빠른 연산에 해당한다. 그러나 여러 요소의 스타일 변경이 동시에 발생할 경우에는 리페인트 비용이 증가할 수 있다. 
그래서 리플로우와 리페인트는 웹페이지에서 UI 업데이트의 비용이 큰 연산으로, 빈번하게 발생하면 웹페이지의 성능이 저하될 수 있다. 따라서, 리액트의 가상 DOM이 실제 DOM 조작을 최소화하는 방식으로 이 비용을 최소화하여 성능을 향상시킬 수 있다.
💡 네이티브 DOM 요소: 웹 브라우저에서 제공되는 기본적인 HTML 요소를 뜻한다. HTML 문서의 각 요소는 DOM 요소로 표현되며, 자바스크립트를 사용하여 해당 요소에 접근하고 조작할 수 있다. div, p, input, button과 같은 HTML 요소는 모두 네이티브 DOM 요소에 해당한다.