본 내용은 자바스크립트+리액트 디자인 패턴(링크) 를 읽고 정리한 내용입니다. 책의 내용과 함께 개인적인 의견과 생각을 담아 작성하였습니다.

  • XML과 유사한 구문을 사용하여 HTML을 자바스크립트에서 사용할 수 있게 해주는 확장 문법
  • 자바스크립트로 변환됨

  • 리액트 컴포넌트의 내부 데이터
  • 컴포넌트가 만들어지기 전에 미리 결정됨
  • 컴포넌트로 전달되고 나면 읽기 전용이 됨

  • 서버에서 생성한 마크업의 UI가 상호작용할 수 있게 만드는 과정
  • 자바스크립트 번들이 로드되고 이벤트 핸들러 등이 DOM에 추가되며 처리

HOC : Higher-Order Component

  • 여러 컴포넌트에서 동일한 로직을 재사용하는 방법 중 하나
  • 애플리케이션 전체에서 컴포넌트 로직을 재사용할 수 있음
  • 다른 컴포넌트를 인자로 받아 새로운 컴포넌트를 반환
  • 인자로 받은 컴포넌트에 추가 기능을 적용한 새로운 컴포넌트를 반환

hook으로도 충분히 같은 효과를 낼 수 있을 것 같은데?

다음과 같은 경우에 효과적일 수 있다.

  • 애플리케이션 전체에 걸쳐 여러 컴포넌트에 동일한 동작을 적용해야 할 때
  • 추가된 커스텀 로직 없이도 컴포넌트가 독립적으로 작동할 수 있을 때

그러니까 이것도 hook도 다 할 수 있는 일 아니냐고요... (알고보니 저자도 알고 있지만 hook 패턴이 나중에 등장해야 해서 그런 거였음)

  • 재사용하고자 하는 로직을 한 곳에 모아 관리할 수 있음
  • 로직을 한 곳에 집중시킴으로써 코드를 DRY하게 유지 가능
  • 효과적으로 관심사 분리 가능

  • 대상 컴포넌트에 전달하는 prop의 이름이 충돌을 일으킬 수 있음

  • 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를 두는 것도 그냥 별도로 컴포넌트를 구성하면 되는 거 아닌가?

  • 여러 컴포넌트 사이에서 로직과 데이터를 쉽게 공유할 수 있음
  • 컴포넌트의 재사용성을 높일 수 있음

  • 리액트 hooks는 렌더링 props 패턴으로 해결할 수 있는 문제 대부분을 이미 해결

역시나 그렇군요. hook의 등장 이전에 존재하던 패턴들이라 hook을 알고 있는 지금의 저로서는 이해가 안 되는 거였습니다.

리액트 16.8 버전에서 도입

  • Class 컴포넌트를 사용하지 않고 상태와 라이프사이클 메서드를 활용 가능
  • 많은 전통적인 디자인 패턴을 대체 가능

  • 함수형 컴포넌트에 상태 추가
  • 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)} />
  )
}

위의 예시처럼 클래스형 컴포넌트에서보다 훨씬 간단하게 함수형 컴포넌트에서 상태를 관리 가능

함수형 컴포넌트 내에서 상태를 업데이트하고 조작

  • 함수형 컴포넌트의 주요 라이프사이클 이벤트 중간에 코드를 실행하는 데에 사용
  • 원래 함수형 컴포넌트의 내부에서는 값 변경, 구독, 타이머, 로깅 등 기타 부수 효과를 사용할 수 없다.(의외 포인트)
    • 이러한 작업이 허용되면 UI에 혼란스러운 버그와 오류를 초래할 수 있기 때문
  • 이 hook 하나로 클래스형 컴포넌트에서 사용하던 라이프사이클 메서드를 대체할 수 있음

  • 컨텍스트 API를 사용해 컴포넌트 트리 전체에 걸쳐 데이터를 쉽게 공유할 수 있음

  • setState의 대안
  • 깊은 트리를 가진 복잡한 상태 로직에 유용
  • 변경 이후의 상태가 이전 상태에 따라 달라지는 경우에 특히 유용
  • 깊은 구조를 가진 컴포넌트의 업데이트 성능을 최적화

  • 클래스는 관리가 어렵고, 핫 리로딩과 함께 사용하기 힘들며, 코드 경량화가 어려움
  • hook은 함수형 프로그래밍을 쉽게 구현할 수 있게 도와줌

  • hook 도입 이전에는 UI와 무관한 로직을 추출하고 공유할 방법이 없었음
  • 때문에 고차 컴포넌트 패턴이나 렌더링 Props와 같은 복잡한 방법을 동원해야 했음
  • 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();
  • 현재 경로에 필요한 코드가 포함된 번들만 요청

  • 최신 웹 애플리케이션 개발에서 웹팩 또는 롤업 등의 번들러는 소스 코드를 하나 이상의 번들 파일로 그룹화
  • 요청된 데이터의 로딩 및 실행 시간 최적화는 여전히 개발자의 몫
  • 메인 스레드를 차단하지 않도록 실행 시간을 최대한 단축해야 함
  • 초기 로딩 시 현재 페이지에서 우선순위가 높지 않은 코드를 요청할 때초기 페이지 렌더링에 필요한 코드와 분리해서 로드하는 것이 좋음

Push, Render, Pre-cache, Lazy-load

  • 어려운 환경에서도 애플리케이션이 최대한 효율적으로 로드될 수 있도록 하기
  • Push: 중요한 리소스를 효율적으로 푸시하여 서버 왕복 횟수 및 로딩 시간 단축
  • Render: 초기 경로를 최대한 빠르게 렌더링
  • Pre-cache: 자주 방문하는 경로의 에셋을 백그라운드에서 미리 캐싱
  • Lazy-load: 자주 요청되지 않는 경로나 에셋은 지연 로딩

클라이언트와 서버 간의 왕복 횟수를 최소화하는 것이 중요

  • keep-alive 헤더를 사용 : 연결을 유지하여 왕복 횟수 최소화
  • 요청과 응답에 줄바꿈 문자로 구분되는 일반 텍스트 프로토콜 사용
  • 클라이언트와 서버 간 TCP 연결 : 최대 6개
  • 동일한 TCP 연결을 통해 새로운 요청을 보내려면 이전 요청이 완료되어야 함
  • HOL(Head of Line) Blocking : 마지막 요청이 오래 걸리면 다른 요청을 전송할 수 없게 됨

  • 요청과 응답을 작은 프레임으로 분할
  • 양방향 스트림 사용 : 단일 TCP 연결을 통해 여러 개의 양방향 스트림을 만듦
  • 클라이언트-서버 간 여러 개의 요청 및 응답 프레임을 동시에 전달 가능
  • 이전에 보낸 요청이 완료되기 전에 동일한 TCP 연결을 통해 여러 요청을 보낼 수 있음(HOL Blocking 해결)
  • 서버 푸시 : 자동으로 추가 리소스를 전송 가능

  • 리소스 푸시는 추가 리소스를 받는 시간은 줄여줌
  • 하지만 서버 푸시는 HTTP 캐시를 인지하지 못함
  • 푸시된 리소스는 다음에 웹사이트 재방문 시에는 사용 불가, 다시 서버에 요청

  • 초기 로드 이후에 서비스 워커 사용
  • 해당 리소스를 캐시함으로써 클라이언트가 불필요한 요청을 하지 않도록 최적화

  • 브라우저가 어떤 리소스를 먼저 가져와야 하는지 알려주기 위해 중요한 리소스에 preload 힌트를 추가
  • preload는 현재 경로에 중요한 리소스를 로드하는데 걸리는 시간을 최적화
  • 브라우저가 해당 리소스를 발견하는 것보다 더 빨리 가져오게 됨
  • 너무 과용하면 오히려 초기 로드 시간이 늘어날 수 있음
    • 브라우저 캐시는 제한적
    • 불필요한 리소스를 preload 처리하는 경우 불필요하게 대역폭을 많이 사용할 수도
    • 큰 번들을 캐싱하면 여러 번들이 동일한 리소스 대역폭을 공유해서 문제가 될 수 있음

  • 요청하는 번들이 해당 시점에 필요한 최소한의 리소스만 포함
  • 브라우저에서는 리소스를 캐싱할 수 있어야 함
  • 경우에 따라 번들을 전혀 사용하지 않는 것이 더 효율적일 수도

  • Preload(<link rel="preload">)는 브라우저의 최적화 기능
  • 브라우저가 늦게 요청할 수도 있는 중요한 리소스를 더 일직 요청할 수 있도록 함

  • 상호작용에 필요한 리소스를 먼저 로딩하다가 FCP, LCP에 필요한 리소스의 로딩이 지연되는 일은 피하기
  • 자바스크립트 자체의 로딩을 최적화하려면 <body> 태그보다는 <head> 태그 안에서 <script defer>를 사용하는 것이 초기 로딩에 더 효과적

  • 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는 다른 리소스의 다운로드를 지연시킬 수 있지만, 얻을 수 있는 이점을 위해 트레이드오프를 감수
<link rel="preload" href="emoji-picker.bundle.js" as="script">
<script src="emoji-picker.bundle.js" async></script>

preload에 대한 새로운 사용 권장사항

  • HTTP 헤더에 preload를 넣으면 다른 모든 리소스보다 우선적으로 로드
  • 미리 로드되는 폰트는 <head> 태그 끝 부분이나 <body> 태그 시작 부분에 위치
  • 이미지 preload는 기본적으로 우선순위가 낮음
  • 비동기 스크립트 및 기타 낮은/최저 우선순위 태그와 관련하여 순서 지정 필요

대규모 데이터 리스트의 렌더링 성능을 향상시키는 기술

  • 전체 목록을 모두 렌더링하는 대신 현재 화면에 보이는 행만 동적으로 렌더링
  • 사용자가 스크롤함에 따라 보이는 영역(윈도우)이 이동

  • 사용자가 스크롤할 때마다 이전 항목을 윈도우에서 제거하고 새로운 항목으로 대체

  • 최신 브라우저 중 일부는 CSS의 content-visibility 속성을 지원
  • 이 속성은 요소의 내용이 화면에 보이는 경우에만 렌더링하도록 지시
  • content-visibility: auto 속성을 사용하면 화면 밖 컨텐츠의 렌더링과 페인팅을 필요한 시점까지 지연
  • 큰 HTML 문서에서 렌더링 성능을 향상시키는 데 도움이 됨

  • preload는 신중하게 사용해야 함
  • preload를 잘못 사용하면 FCP에 필수적인 리소스(ex. css, font)를 지연시켜 원하는 결과와 반대되는 결과를 초래

preload와 관련해서는 잘 알지 못했는데 배워가는 계기가 되었네요.