TOC
참고
본 내용은 자바스크립트+리액트 디자인 패턴(링크) 를 읽고 정리한 내용입니다. 책의 내용과 함께 개인적인 의견과 생각을 담아 작성하였습니다.
리액트
리액트의 기본 개념
JSX
- XML과 유사한 구문을 사용하여 HTML을 자바스크립트에서 사용할 수 있게 해주는 확장 문법
- 자바스크립트로 변환됨
Props
- 리액트 컴포넌트의 내부 데이터
- 컴포넌트가 만들어지기 전에 미리 결정됨
- 컴포넌트로 전달되고 나면 읽기 전용이 됨
하이드레이션
- 서버에서 생성한 마크업의 UI가 상호작용할 수 있게 만드는 과정
- 자바스크립트 번들이 로드되고 이벤트 핸들러 등이 DOM에 추가되며 처리
고차 컴포넌트
HOC : Higher-Order Component
고차 컴포넌트의 개념
- 여러 컴포넌트에서 동일한 로직을 재사용하는 방법 중 하나
- 애플리케이션 전체에서 컴포넌트 로직을 재사용할 수 있음
- 다른 컴포넌트를 인자로 받아 새로운 컴포넌트를 반환
- 인자로 받은 컴포넌트에 추가 기능을 적용한 새로운 컴포넌트를 반환
나의 생각
hook으로도 충분히 같은 효과를 낼 수 있을 것 같은데?
고차 컴포넌트의 활용
다음과 같은 경우에 효과적일 수 있다.
- 애플리케이션 전체에 걸쳐 여러 컴포넌트에 동일한 동작을 적용해야 할 때
- 추가된 커스텀 로직 없이도 컴포넌트가 독립적으로 작동할 수 있을 때
나의 생각
그러니까 이것도 hook도 다 할 수 있는 일 아니냐고요... (알고보니 저자도 알고 있지만 hook 패턴이 나중에 등장해야 해서 그런 거였음)
고차 컴포넌트의 장단점
장점
- 재사용하고자 하는 로직을 한 곳에 모아 관리할 수 있음
- 로직을 한 곳에 집중시킴으로써 코드를 DRY하게 유지 가능
- 효과적으로 관심사 분리 가능
단점
- 대상 컴포넌트에 전달하는 prop의 이름이 충돌을 일으킬 수 있음
렌더링 Props 패턴
- JSX 요소를 반환하는 함수 값을 가지는 컴포넌트의 prop
- 컴포넌트 자체는 렌더링 prop 외에는 아무 것도 렌더링하지 않음
- 자신의 렌더링 로직을 구현하는 대신, 렌더링 prop을 호출
const Title = props => props.render()
...
<Title render={() => <h1>Hello</h1>} />
...
- 장점 : prop을 받는 컴포넌트를 재사용할 수 있음
상태 끌어올리기
const Input = (props) => {
const [value, setValue] = useState('')
return (
<>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder=...
/>
{props.render(value)}
</>
)
}
const App = () => {
return (
<div className="App">
<h1>Temperature Converter</h1>
<Input
render={(value) => (
<>
<Kelvin value={value} />
<Fahrenheit value={value} />
</>
)}
/>
</div>
)
}
나의 생각
이게 children으로 넘기는 것보다 어떤 장점을 가지는지 잘 모르겠다. Input 내에 state를 두는 것도 그냥 별도로 컴포넌트를 구성하면 되는 거 아닌가?
렌더링 Props의 장단점
장점
- 여러 컴포넌트 사이에서 로직과 데이터를 쉽게 공유할 수 있음
- 컴포넌트의 재사용성을 높일 수 있음
단점
- 리액트 hooks는 렌더링 props 패턴으로 해결할 수 있는 문제 대부분을 이미 해결
나의 생각
역시나 그렇군요. hook의 등장 이전에 존재하던 패턴들이라 hook을 알고 있는 지금의 저로서는 이해가 안 되는 거였습니다.
리액트 hooks 패턴
리액트 16.8 버전에서 도입
- Class 컴포넌트를 사용하지 않고 상태와 라이프사이클 메서드를 활용 가능
- 많은 전통적인 디자인 패턴을 대체 가능
hook이 가능하게 한 것
- 함수형 컴포넌트에 상태 추가
- componentDidMount, componentDidUpdate, componentWillUnmount 등의 클래스형 컴포넌트에서 사용하던 라이프사이클 메서드를 사용하지 않고도 컴포넌트 라이프사이클 관리
- 여러 컴포넌트 간에 동일한 상태 관련 로직 재사용
클래스 컴포넌트와의 비교
// 클래스형 컴포넌트
class Input extends React.Component {
constructor(props) {
super(props)
this.state = { value: '' }
this.handleInput = this.handleInput.bind(this)
}
handleInput(e) {
this.setState({ value: e.target.value })
}
render() {
return (
<input type="text" value={this.state.value} onChange={this.handleInput} />
)
}
}
// 함수형 컴포넌트
const Input = () => {
const [value, setValue] = useState('')
return (
<input type="text" value={value} onChange={(e) => setValue(e.target.value)} />
)
}
위의 예시처럼 클래스형 컴포넌트에서보다 훨씬 간단하게 함수형 컴포넌트에서 상태를 관리 가능
hook 관련 추가 정보
useState
함수형 컴포넌트 내에서 상태를 업데이트하고 조작
useEffect
- 함수형 컴포넌트의 주요 라이프사이클 이벤트 중간에 코드를 실행하는 데에 사용
- 원래 함수형 컴포넌트의 내부에서는 값 변경, 구독, 타이머, 로깅 등 기타 부수 효과를 사용할 수 없다.(의외 포인트)
- 이러한 작업이 허용되면 UI에 혼란스러운 버그와 오류를 초래할 수 있기 때문
- 이 hook 하나로 클래스형 컴포넌트에서 사용하던 라이프사이클 메서드를 대체할 수 있음
useContext
- 컨텍스트 API를 사용해 컴포넌트 트리 전체에 걸쳐 데이터를 쉽게 공유할 수 있음
useReducer
- setState의 대안
- 깊은 트리를 가진 복잡한 상태 로직에 유용
- 변경 이후의 상태가 이전 상태에 따라 달라지는 경우에 특히 유용
- 깊은 구조를 가진 컴포넌트의 업데이트 성능을 최적화
hook의 장점
복잡한 컴포넌트의 단순화
- 클래스는 관리가 어렵고, 핫 리로딩과 함께 사용하기 힘들며, 코드 경량화가 어려움
- hook은 함수형 프로그래밍을 쉽게 구현할 수 있게 도와줌
UI에서 분리된 로직 공유
- hook 도입 이전에는 UI와 무관한 로직을 추출하고 공유할 방법이 없었음
- 때문에 고차 컴포넌트 패턴이나 렌더링 Props와 같은 복잡한 방법을 동원해야 했음
- hook의 등장 이후 상태 관련 로직을 단순한 자바스크립트 함수로 추출할 수 있게 해주며 문제를 해결
hook의 단점
- hook 사용 규칙을 준수해야 함(Linter 플러그인 사용 시 규칙 위반 확인에 용이)
- 올바르게 사용하려면 상당한 연습이 필요 (ex. useEffect)
- 잘못된 사용에 주의해야 함 (ex. useCallback, useMemo)
나의 생각
글의 작성 시기가 언제인지 모르겠으나 클래스형 컴포넌트와의 비교, 그리고 이전의 패턴들을 자꾸 언급하고 비교하는 것을 보며 hook의 등장이 오래 되지 않았을 때 작성된 글인가 하는 생각을 했어요. 압도적인 hook의 기능으로 이제는 패러다임이 확실히 넘어온 것 같아요. 클래스형의 아쉬운 부분을 역체감하는 기회가 되었어요.
정적 가져오기
- 정적으로 가져오는 모든 모듈은 초기 번들에 추가
import module from 'module'
구문을 사용하여 모듈을 가져옴
// 정적으로 가져옴
import UserInfo from './components/UserInfo';
import ChatList from './components/ChatList';
import ChatInput from './components/ChatInput';
const App = () => {
return (
<div className="App">
<UserInfo />
<ChatList />
<ChatInput />
</div>
)
}
- 각 모듈은 자바스크립트 엔진이 해당 모듈을 import하는 코드에 도달하는 즉시 실행
- 웹팩은 이 모듈들을 초기 번들에 포함
동적 가져오기
"모든 모듈을 한 번에 가져올 필요는 없다!"
- 사용자 상호작용에 따라서만 렌더링되거나, 페이지 하단에 위치하는 모듈은 나중에 가져와도 됨
- 모듈을 동적으로 가져오면 초기 번들 크기를 줄일 수 있음
// 동적으로 가져옴
import React, { Suspense, lazy } from 'react';
const Send = lazy(() => import(/* webpackChunkName: "send-icon" */ './icons/Send'));
const Emoji = lazy(() => import(/* webpackChunkName: "emoji-icon" */ './icons/Emoji'));
const Picker = lazy(() => import(/* webpackChunkName: "emoji-picker" */ './components/EmojiPicker'));
const ChatInput = () => {
const [pickerOpen, togglePicker] = useReducer(state => !state, false);
return (
<Suspense fallback={<p>Loading...</p>}>
<div className="chat-input-container">
<input type="text" placeholder="Message..." />
<Send />
<Emoji onClick={togglePicker} />
{pickerOpen && <Picker />}
</div>
</Suspense>
)
}
- 위의 예시에서 사용자가 이모지를 클릭할 때
EmojiPicker
컴포넌트가 동적으로 로드됨 - 이런 유형의 동적 가져오기를 **'상호작용 시 가져오기(import on interaction)'**라고 함
화면에 보이는 순간 가져오기
import on Visibility
- 초기 페이지 로드 시에는 보이지 않아도 되는 컴포넌트들이 있음
- 이들이 화면에 보일 때 동적으로 가져오는 것
IntersectionObserver
API를 사용
코드 스플리팅
경로 기반 분할
- 특정 페이지나 경로에서만 필요한 리소스
import React, { lazy } from 'react';
import { render } from 'react-dom';
import { Switch, Route, BrowserRouter as Router } from 'react-router-dom';
const Home = lazy(() => import(/* webpackChunkName: "home" */ './pages/Home'));
const About = lazy(() => import(/* webpackChunkName: "about" */ './pages/About'));
const Overview = lazy(() => import(/* webpackChunkName: "overview" */ './pages/Overview'));
render(
<Router>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" component={About} />
<Route path="/overview" component={Overview} />
</Switch>
</Router>,
document.getElementById('root')
)
module.hot.accept();
- 현재 경로에 필요한 코드가 포함된 번들만 요청
번들 분할
- 최신 웹 애플리케이션 개발에서 웹팩 또는 롤업 등의 번들러는 소스 코드를 하나 이상의 번들 파일로 그룹화
- 요청된 데이터의 로딩 및 실행 시간 최적화는 여전히 개발자의 몫
- 메인 스레드를 차단하지 않도록 실행 시간을 최대한 단축해야 함
- 초기 로딩 시 현재 페이지에서 우선순위가 높지 않은 코드를 요청할 때는 초기 페이지 렌더링에 필요한 코드와 분리해서 로드하는 것이 좋음
PRPL 패턴
Push, Render, Pre-cache, Lazy-load
- 어려운 환경에서도 애플리케이션이 최대한 효율적으로 로드될 수 있도록 하기
- Push: 중요한 리소스를 효율적으로 푸시하여 서버 왕복 횟수 및 로딩 시간 단축
- Render: 초기 경로를 최대한 빠르게 렌더링
- Pre-cache: 자주 방문하는 경로의 에셋을 백그라운드에서 미리 캐싱
- Lazy-load: 자주 요청되지 않는 경로나 에셋은 지연 로딩
클라이언트와 서버 간의 왕복 횟수를 최소화하는 것이 중요
HTTP/1.1
- keep-alive 헤더를 사용 : 연결을 유지하여 왕복 횟수 최소화
- 요청과 응답에 줄바꿈 문자로 구분되는 일반 텍스트 프로토콜 사용
- 클라이언트와 서버 간 TCP 연결 : 최대 6개
- 동일한 TCP 연결을 통해 새로운 요청을 보내려면 이전 요청이 완료되어야 함
- HOL(Head of Line) Blocking : 마지막 요청이 오래 걸리면 다른 요청을 전송할 수 없게 됨
HTTP/2
- 요청과 응답을 작은 프레임으로 분할
- 양방향 스트림 사용 : 단일 TCP 연결을 통해 여러 개의 양방향 스트림을 만듦
- 클라이언트-서버 간 여러 개의 요청 및 응답 프레임을 동시에 전달 가능
- 이전에 보낸 요청이 완료되기 전에 동일한 TCP 연결을 통해 여러 요청을 보낼 수 있음(HOL Blocking 해결)
- 서버 푸시 : 자동으로 추가 리소스를 전송 가능
리소스 푸시에 대하여
- 리소스 푸시는 추가 리소스를 받는 시간은 줄여줌
- 하지만 서버 푸시는 HTTP 캐시를 인지하지 못함
- 푸시된 리소스는 다음에 웹사이트 재방문 시에는 사용 불가, 다시 서버에 요청
PRPL의 해결 방법
- 초기 로드 이후에 서비스 워커 사용
- 해당 리소스를 캐시함으로써 클라이언트가 불필요한 요청을 하지 않도록 최적화
preload
- 브라우저가 어떤 리소스를 먼저 가져와야 하는지 알려주기 위해 중요한 리소스에 preload 힌트를 추가
- preload는 현재 경로에 중요한 리소스를 로드하는데 걸리는 시간을 최적화
- 브라우저가 해당 리소스를 발견하는 것보다 더 빨리 가져오게 됨
- 너무 과용하면 오히려 초기 로드 시간이 늘어날 수 있음
- 브라우저 캐시는 제한적
- 불필요한 리소스를 preload 처리하는 경우 불필요하게 대역폭을 많이 사용할 수도
- 큰 번들을 캐싱하면 여러 번들이 동일한 리소스 대역폭을 공유해서 문제가 될 수 있음
PRPL 패턴을 적용할 때에는
- 요청하는 번들이 해당 시점에 필요한 최소한의 리소스만 포함
- 브라우저에서는 리소스를 캐싱할 수 있어야 함
- 경우에 따라 번들을 전혀 사용하지 않는 것이 더 효율적일 수도
로딩 우선순위
- Preload(
<link rel="preload">
)는 브라우저의 최적화 기능 - 브라우저가 늦게 요청할 수도 있는 중요한 리소스를 더 일직 요청할 수 있도록 함
preload의 주의점
- 상호작용에 필요한 리소스를 먼저 로딩하다가 FCP, LCP에 필요한 리소스의 로딩이 지연되는 일은 피하기
- 자바스크립트 자체의 로딩을 최적화하려면
<body>
태그보다는<head>
태그 안에서<script defer>
를 사용하는 것이 초기 로딩에 더 효과적
SPA의 Preload
- Prefetching은 곧 요청될 가능성이 있는 리소스를 캐시하는 좋은 방법
- 즉시 사용해야 하는 리소스의 경우에는 preload를 사용
const EmojiPicker = import(/* webpackPreload: true */ './components/EmojiPicker');;
<link rel="prefetch" href="emoji-picker.bundle.js" as="script">
<link rel="prefetch" href="vendors~emoji-picker.bundle.js" as="script">
- prefetch : 브라우저가 인터넷 연결 상태와 대역폭을 고려해 어떤 리소스를 미리 가져올지 결정
- preload : 미리 로드된 리소스는 어떤 상황에서든 무조건 미리 로드
Preload + async 기법
- 브라우저가 스크립트를 높은 우선순위로 다운로드하면서도, 스크립트를 기다리는 동안 파싱이 멈추지 않도록 하는 기법
- preload는 다른 리소스의 다운로드를 지연시킬 수 있지만, 얻을 수 있는 이점을 위해 트레이드오프를 감수
<link rel="preload" href="emoji-picker.bundle.js" as="script">
<script src="emoji-picker.bundle.js" async></script>
크롬 95+ 버전에서의 Preload
preload에 대한 새로운 사용 권장사항
- HTTP 헤더에 preload를 넣으면 다른 모든 리소스보다 우선적으로 로드
- 미리 로드되는 폰트는
<head>
태그 끝 부분이나<body>
태그 시작 부분에 위치 - 이미지 preload는 기본적으로 우선순위가 낮음
- 비동기 스크립트 및 기타 낮은/최저 우선순위 태그와 관련하여 순서 지정 필요
리스트 가상화
대규모 데이터 리스트의 렌더링 성능을 향상시키는 기술
- 전체 목록을 모두 렌더링하는 대신 현재 화면에 보이는 행만 동적으로 렌더링
- 사용자가 스크롤함에 따라 보이는 영역(윈도우)이 이동
작동 방식
- 사용자가 스크롤할 때마다 이전 항목을 윈도우에서 제거하고 새로운 항목으로 대체
content-visibility
- 최신 브라우저 중 일부는 CSS의
content-visibility
속성을 지원 - 이 속성은 요소의 내용이 화면에 보이는 경우에만 렌더링하도록 지시
content-visibility: auto
속성을 사용하면 화면 밖 컨텐츠의 렌더링과 페인팅을 필요한 시점까지 지연- 큰 HTML 문서에서 렌더링 성능을 향상시키는 데 도움이 됨
결론
양날의 검, preload
- preload는 신중하게 사용해야 함
- preload를 잘못 사용하면 FCP에 필수적인 리소스(ex. css, font)를 지연시켜 원하는 결과와 반대되는 결과를 초래
나의 생각
preload와 관련해서는 잘 알지 못했는데 배워가는 계기가 되었네요.