2022년 마지막 프로젝트의 회고 5 — The rest
HTML 포커스, React ref, TypeScript, CSR·SSR·SSG·ISR을 익히며 남긴 프로젝트 회고.
Proem
앞서 작성한 것들 외에, 따로 섹션을 나누기 어려웠던, 익히고 배우게 된 것들에 대한 이야기들을 풀어보려 한다.
dataset Attribute in HTML
해당 HTML 요소에 data-dataName='dataValue'처럼, 이름과 값으로 속성을 부여해주면, 이벤트에서 event.currentTarget.dataset['이름']으로 접근할 수 있다.
이를 이용해 리뷰 페이지에서 평점을 매기는 컴포넌트에서 별 모양 버튼을 클릭해 해당 별 만큼의 평점을 부여하여 평가를 남기는 기능을 간단하게 구현할 수 있었다.
input, button, 그리고 다른 HTML 요소의 포커스
상품이나, 게시글같이 콘텐츠의 세부 페이지로 연결할 때, <article />, <div />등을 콘텐츠의 컨테이너 혹은 래퍼로 활용했는데, <input />, <button /> 등의 요소 외에는 포커스가 잡히지 않았다. 웹 접근성의 측면에서, 마우스 같은 인터페이스 장치가 없는 경우, 키보드만 이용해서도 서비스를 이용할 수 있어야 한다고 생각했고, 그래서 이를 타파하고자 했다.
우선, HTML 요소에 tabIndex 속성을 부여해서 포커스가 가능한 요소로 만들 수 있고, onKeyDown 이벤트를 이용하면 이를 통해 상호작용이 가능한 컴포넌트로 만들 수 있었다.
const _onKeyDown = (e: KeyboardEvent) => {
if (e.code === "Enter") {
e.currentTarget.parentElement?.click();
}
};
...
<ImageInput register={register("avatar")}>
<span tabIndex={0} onKeyDown={_onKeyDown}>
프로필 사진 바꾸기
</span>
</ImageInput>위의 예시처럼, 이미지 선택에 대한 <input /> 요소를 <label /> 요소 안에 숨겨두고, ‘프로필 사진 바꾸기’라는 요소를 버튼처럼 포커스를 주고, 액세스 시, 부모 요소인 <label /> 을 클릭한 것처럼 만드는 방식으로 활용했다.
초기에는 이렇게 간단하게 이벤트를 구현해서 쓰다가, 반복 사용이 잦아져 아래와 같이 Hook으로 만들어서 사용했다. 또한, 부모 요소 뿐만 아니라 포커스된 요소 자기 자신을 클릭하는 경우도 같이 구현했다.
import type { KeyboardEvent } from "react";
type target = "itself" | "parent";
const useFocusEvent = (target: target = "parent") => {
let onKeyDownEnter: (e: KeyboardEvent) => void;
if (target === "itself") {
onKeyDownEnter = (e: KeyboardEvent) => {
if (e.code === "Enter") {
e.currentTarget.firstElementChild?.parentElement?.click();
}
};
} else {
onKeyDownEnter = (e: KeyboardEvent) => {
if (e.code === "Enter") {
e.currentTarget.parentElement?.click();
}
};
}
return { onKeyDownEnter };
};
export default useFocusEvent;Refs in React
바닐라 자바스크립트로 DOM의 특정 엘리먼트를 조작할 때는 getElementById, querySelector 같은 함수를 사용해 DOM을 직접 선택 및 할당해서 조작을 하곤 한다.
React에선 이와 달리, ref를 통해 DOM의 요소를 선택할 수 있다. createRef 함수, 혹은 useRef라는 React Hook을 통해 ref 객체를 만들고, ref 하위의 current 값을 통해 DOM 요소를 가리킬 수 있다. 또한, useRef 훅을 사용할 때는 내부에 매개 변수를 넣어주면 이 값이 current 값의 기본값이 된다.
나는 이번 프로젝트에서 모달과 사이드바가 나타날 때 포커스를 주기 위한 솔루션으로 이 기능을 사용할 수 있었다.
이렇게 특정 DOM 요소를 선택하는 것 외에도 useRef 훅의 기능이 하나 더 있는데, 이는 바로 컴포넌트 내에서 조회 및 수정할 수 있는 변수를 관리하는 것으로, useRef는 마치 current 속성에 변경 가능한 값을 담고 있는 상자와 같다고 볼 수 있다.
React 공식 기술 문서에서는 ‘현재 프로퍼티가 변경할 수 있고, 어떤 값이든 보유할 수 있는 일반 컨테이너로, 이는 class의 인스턴스 프로퍼티와 유사’하다고 표현하고 있다.
이렇게 useRef Hook으로 관리하는 변수는 내용이 변경될 때, 그것을 알려주지 않으며, current 속성을 변형하는 것이 리렌더링을 발생시키지 않는다고 한다. 즉, 값이 바뀌어도 컴포넌트가 리렌더링이 되지 않는다. 그러므로 어떤 가변 값을 유지하는 데에 편리하다.
useRef & TypeScript
개인적으로 TypeScript를 굉장히 좋아하기에, useRef Hook을 TypeScript와 함께 쓰기 위해 타입 분석도 해봤었다.

이렇게 컴포넌트 내의 변수를 관리하는 목적으로 값을 할당하면, 해당하는 타입의 MutableRefObject<T>를 반환한다. 즉, useRef에 할당한 제네릭이 곧 current 속성의 타입이 된다.


그리고 DOM 요소를 선택할 때는 해당 요소의 타입을 제네릭으로 주고, 초기화를 위해 인자로 보통 null을 할당하게 된다. 여기서 undefined 할당 혹은, 괄호 내부를 빈 칸으로 둘 수 없는 이유는, DOM 엘리먼트의 ref 속성의 타입이 LegacyRef<T> | undefined이며, 여기서 LegacyRef<T>의 타입은 string | Ref<T>이고, 다시 Ref<T>의 타입은 RefCallback<T> | RefObject<T> | null 이기 때문이다. undefined가 없다.
다시 돌아가서, 이렇게 받은 ref 객체는 RefObject<T>의 타입을 갖고, 이 객체의 current 속성은 null, 혹은 useRef에 할당한 제네릭으로 타입을 갖지만 읽기 전용이라는 점이 다르다.
또한 React는 컴포넌트 기반의 라이브러리답게, forwardRef 함수를 이용하면 이 ref를 하위의 컴포넌트로 전달할 수도 있다. 공식 기술 문서에 이에 대한 예제와 프로세스가 자세하게 나와있다.
CSR, SSR, SSG, ISR
Next.js는 다양한 렌더링 방식의 옵션을 제공한다.
어떠한 설정 없이 기본적으로 적용된 방식은 CSR(Client Side Rendering)이다. 이는 사용자에게 빈 HTML 파일을 보내주고, CSS와 스크립트 파일을 동적으로 다운로드하여 브라우저 요소를 렌더링 하는 방식이다. 스크립트를 로드하고, 브라우저가 이를 해석하여 렌더링 하는 동안, 사용자는 페이지의 뼈대만 — 혹은 렌더링이 끝난 컴포넌트들을 순차적으로 — 보게 된다.
이는 자연스러운 사용자 경험을 제공하고, 페이지의 완전한 로드에는 시간이 비교적 오래 소모될 수 있지만, 상호 작용 시 사용자의 행동에 따라 필요한 리소스만 추가로 로드하므로 전체 페이지를 로드하는 것보다 빠르게 반응할 수 있으며, 서버에게 주는 부하도 적다. 물론, 사용자에게 스크립트 파일을 로드해야 하는 부담을 전가하게 되고, 이에 더해 사용자의 네트워크 환경이 좋지 않다면, 사용자 경험의 하락을 야기할 것이다. 또한, 초기에 빈 페이지를 전달함으로 인해 검색 엔진 최적화와는 거리가 멀어진다.
getServerSideProps 옵션을 이용하면 SSR(Server Side Rendering) 방식도 가능해진다. 이는 CSR 과는 달리, 서버에서 페이지를 렌더링 해서 사용자에게 내려주어 페이지를 렌더링 하는 시간이 단축되고, 이렇게 페이지가 만들어져 사용자에게 전달되기 때문에 검색 엔진 최적화에도 유리하다. 하지만, 클라이언트 하에서 렌더링 하는 것이 아니므로, 서버의 부하가 보다 늘어나고, 추가적으로 필요한 부분의 로드가 필요할 때, 즉 리렌더링 시 깜빡임이 발생한다. 또한, 일반적으로 SPA 애플리케이션을 작성하는 것보다 복잡하고 난해해 비교적으로 쉽지 않은 것도 사실이다.
getStaticProps 옵션은 Next 애플리케이션을 SSG(Static Site Generation)로 보여줄 수 있게끔 해준다. SSG란, 빌드 타임에 미리 페이지들을 생성해두고, 사용자가 호출 시에 생성해둔 페이지를 보여주는 방식으로, 빌드 타임이 늘어날 순 있으나, 페이지의 응답과 전환이 굉장히 빠르다는 장점이 있다. 이에 더해, getStaticPaths 함수를 이용하면 [id].tsx 같은 동적인 라우팅도 가능하다.
물론, 이는 말 그대로 정적 사이트 생성이므로, 데이터의 실시간 반영이 어렵다는 단점이 있다. 하지만 이에 대한 솔루션으로 ISR(Incremental Static Regeration) 옵션을 제공한다.
ISR은 전체 사이트를 다시 빌드 할 필요 없이, 각각의 페이지 별로 정적 재생성을 시킬 수 있다. 이 옵션을 활성화시키면, 해당 페이지의 첫 방문자는 사전에 캐싱된 페이지를 보게 되지만, Next.js가 백그라운드에서 정적 재생성을 트리거하고, 생성이 완료되면 기존의 캐시를 무효화하면서 다음 방문자부턴 새로 생성된 페이지를 표시해 준다. 이 옵션에 더해 ODR(On-Demand Revalidation ISR)을 같이 이용한다면, 사용자가 데이터를 조작했을 때, 이를테면 사용자가 게시물을 등록했을 때와 같은 상황에서 정적 재생성을 트리거 시킬 수 있다.
이번 프로젝트에서는 무한 스크롤링 기능으로 페이지네이션을 구현하고 싶었다. 마침 캐싱 라이브러리로 SWR를 사용하고 있었고, 이 라이브러리에서 제공하는 useSWRInfinite API로 이를 어렵지 않게 구현해냈다. 이후, ODR을 통한 ISR의 장점과 훌륭한 사용자 경험을 제공하는 무한 스크롤링 인터랙션을 둘 다 누리고 싶었지만, 생각해 보니 무한 스크롤링은 CSR 환경에서 가능하므로 그만두게 되었다.
하지만 프로젝트를 마무리하고 글을 쓰며 생각해 보니, 레이아웃과 초기에 보여줄 데이터만 ISR로 구현하고, 추가로 받게 되는 요청에 대해서는 CSR을 이용해 응답하도록 구현한다면 가능하지 않았을까. 정말 배운 게 많고, 만족도도 높았던 프로젝트였지만, 몇 가지 아쉬운 점은 분명 존재했는데, 이게 그중 한 가지가 아닐까 싶다.
References
tabindex - HTML: Hypertext Markup Language | MDN
Data Fetching: Overview | Next.js
원문: Medium