logo
Search검색어를 포함하는 게시물들이 최신순으로 표시됩니다.
    Table of Contents
    12장: 리액트 디자인 패턴

    이미지 보기

    12장: 리액트 디자인 패턴

    리액트 디자인 패턴에 대해 알아보자.

    • 24.12.03 작성

    • 읽는 데 22

    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와 관련해서는 잘 알지 못했는데 배워가는 계기가 되었네요.

    profile

    FE Developer 박승훈

    노력하는 자는 즐기는 자를 이길 수 없다