스타트업에서 MVP를 만드는 알바를 할 때 정해진 스택이 있었다.

 

TypeScript, Next.js, 그리고 Zustand.

 

그 당시 세 가지를 모두 처음 사용해본 터라 부랴부랴 검색해가며 공부와 개발을 병행했던 기억이 있다.

 

시키니까 일단 사용은 하는데... 사실 잘 이해하지 못하고 주도해주셨던 프론트 팀장님 말 듣고 사용하라는 데로 사용하기만 한 기억이 있다.

 

그래서 스타트업에 특화 된 어떤 장점이 있으니까 사용했을 거라는 생각이 들어 미리 좀 알아보고자 한다.

 


 

 

Zustand 바로가기

 

Zustand

 

zustand-demo.pmnd.rs

 

내가 생각하는 Zustand의 가장 큰 장점은 로고가 귀엽다 .. 다른 상태 관리 도구들은 너무 공대틱한 느낌이 심한데 갑자기 곰돌이가 있어서 큐티한 느낌이 난다.

 

홈페이지에 들어가봐도 큐티한 곰돌이 월페이퍼에 어떻게 코드를 작성하는 지 간단한 카운터 예시가 들어가 있다.

 

코드를 보고 어떻게 사용하는 지 한 번 분석해 봤다.

 

import { create } from 'zustand'

const useStore = create((set) => ({
  count: 1,
  inc: () => set((state) => ({ count: state.count + 1 })),
}))

function Counter() {
  const { count, inc } = useStore()
  return (
    <div>
      <span>{count}</span>
      <button onClick={inc}>one up</button>
    </div>
  )
}

 

 

zustand에서 create를 import합니다

 

그리고 useStore라는 함수를 만들어 create 전역 스토어를 설정합니다

 

기본 상태 count의 초기값을 1로 정의하고, inc 액션 함수를 정의합니다

 

set 함수를 사용하여 현재 상태인 1을 업데이트하네요

 

카운터 컴포넌트는 위에서 만든 useStore 함수를 호출해 count와 inc 함수를 가져오네요

 

그리고 그 상태인 count를 화면에 표시합니다

 

onClick에는 useStore에서 만든 inc 함수를 넣을 수 있네요

 

이것으로 간략하게 어떻게 Zustand가 돌아가는 지 이해 했습니다

 

 


 

Zustand는 작고 빠르며 React 프로젝트에서 사용하는 상태관리 라이브러리입니다.

 

아래는 간략하게 정리한 Zustand의 특징들입니다.

 

1. 사용하기 쉬운 API이다

Zustand의 API는 매우 간단하고 직관적이다. 복잡한 설정이 필요 없으며 create 함수를 통해 상태와 상태 변경 함수를 쉽게 정의할 수 있습니다. 또한, 전역 상태와 로컬 상태를 분리하거나 다양한 상태를 혼합해서 사용할 수 있는 유연성을 제공합니다.

 

2. 최소한의 러닝커브를 가진다

다른 상태 관리 라이브러리와 비교해 상태 정의와 구성이 쉬워 비용이 적게 듭니다.

 

3. 선택적으로 구독할 수 있다

특정 상태에만 구독할 수 있는 기능을 제공합니다. 즉, 컴포넌트는 필요한 상태 부분만 구독하여 렌더링 성능을 최적화할 수 있으며, 상태가 변경될 때마다 전체 컴포넌트가 리렌더링되지 않도록 방지합니다.

 

4. 비동기 로직에 대한 간편한 지원

비동기 함수나 동기화가 필요한 상태 관리도 쉽게 지원합니다. useStore 훅을 통해 간단하게 비동기 작업을 정의할 수 있으며, Redux의 Middleward와 유사한 기능도 포함되어 있어 복잡한 비동기 로직을 쉽게 관리할 수 있습니다.

 

5. Context API와의 통합이 필요 없음

Zustand는 React의 Context API를 사용하지 않으므로, 성능상의 이점을 누릴 수 있습니다. 상태를 전역적으로 사용할 때 Context API의 단점을 피하면서도 상태에 직접 접근할 수 있는 구조입니다.

 

6. 크기와 성능

Zustand는 가볍고 빠르며, 특히 웹이나 모바일 애플리케이션에서 사용하기가 적합합니다. 크기가 작은 만큼 로딩 속도가 빠르고, 상태 변경 시 최적화 된 구독 구조 덕분에 불필요한 리렌더링이 발생하지 않아 성능에 큰 이점을 줍니다.

 

 

이렇게 언뜻 보면 다른 상태 관리 도구들과 함께 옵션에 있는 경우에도 Zustand를 선택해야 하는 이유를 딱 찾지는 못하겠네요 ..

 

 

 

그럼 얘를 왜 써요..?

 

 


 


빠르고 가벼운 전역 상태 관리가 필요한 컴포넌트 기반 애플리케이션
비동기 API 데이터를 효율적으로 캐싱하고 필요한 곳에만 제공해야하는 경우
Context API의 한계를 경험하며 단순하지만 유연한 전역 상태 관리가 필요한 경우
다수의 독립적인 상태 관리가 필요한 복잡한 구조의 프로젝트


 

Zustand는 가벼운 API와 성능 최적화 덕분에 간단한 전역 상태 관리를 빠르고 효율적으로 제공합니다. 특히, 상태를 컴포넌트 간에 빠르고 공유할 수 있으며 React Context의 단점인 전체 리렌더링을 피해 필요한 부분에만 상태를 제공하는 선택적 구독을 할 수 있습니다.

 

결론적으로 Zustand의 핵심 키워드는 선택적 구독이라는 생각이 드는데요. 리액트를 공부하면서 코드 최적화를 통해 전체 리렌더링을 줄이는 방법을 공부한 적이 있기 때문에 zustand가 얼마나 효율적일 지 궁금하기도 합니다. 

 

하지만 Recoil도 atom과 selector 단위로 상태를 관리하기 때문에 선택적 구독이 가능해 익숙한 Recoil에 더 손이 많이 갈 것 같다는 생각을 합니다. 두 방식에 약간의 차이는 있다고 합니다.

 

- Recoil은 상태 의존성 그래프를 생성하여 상태를 세밀하게 구독하고 업데이트를 전달합니다.

 

- Zustand는 선택적 구독과 독립적인 스토어 개념을 사용해, 상태를 단순하게 관리하면서도 필요한 컴포넌트에만 상태를 전달하는 방식으로 성능을 최적화합니다.

 

사실 지난 번 Recoil을 공부할 때 다크모드/라이트모드 등을 예시로 들었었는데 단순 가벼운 전역 상태 관리를 위한 도구로는 zustand가 더 어울린다는 상황이 드네요.

Recoil은 그보다는 조금 더 복잡한 여러 개를 선택하는 필터 옵션, 정렬 옵션 등에 더 어울리는 것 같습니다.

 

 

상황에 맞는 상태 관리 쓰기!! 중요 체크체크 

 

 

 

 

Recoil은 유일하게 내가 주도적으로 사용한 경험이 있는 상태 관리 라이브러리이다. 

 

이번 졸업 프로젝트에 적용하기 위해서 간단하고 빠르게 익혀 사용할 수 있는 상태 관리 도구가 필요했는데, 지난 번 타 프로젝트에서 누군가 손쉽게 리코일을 추가해 사용법을 알려줬던 기억이 나 빠르게 프로젝트에 적용해 보기로 했다.

 

 

내가 리코일을 필요로 했던 상황은 아래와 같다.

 

1. 페이지에서 내용들을 등록하다가 다른 페이지에 갔다가 돌아오는 경우 

"작업하시던 내용을 그대로 이어하시겠습니까?" 같은 프로세스가 필요했기 때문에

내용을 저장해둬야 했다.

 

2. 한 페이지 안에서 두 단계를 거쳐 행사를 등록하는 과정을 거치는데 이 과정에서 앞에서 입력받은 내용이 다음 단계에 영향을 끼치는 동시에 왼쪽에 위치한 네비게이션 바에도 영향을 끼쳐야 한다.

 

위와 같은 이유로 단순히 입력받은 내용을 매개변수로 넘기는 것보다는 전역 상태로 저장해두는 것이 사용하기에 편리하다고 생각이 들었다.

 


 

 

Recoil

 

리코일 공식 홈페이지

 

Recoil

A state management library for React.

recoiljs.org

 

Recoil은 React 애플리케이션을 위한 상태 관리 라이브러리이다. 이는 React의 기본적인 상태 관리 영역에서 확장되어, 특히 복잡하고 대규모의 서비스에서 효율적인 상태 관리를 가능하게 한다.

 

Recoil은 FaceBook에서 개발 되었으며, React의 기존 개념과 잘 통합되도록 설계되었다.

 

Recoil을 사용하면 atoms에서 selectors를 거쳐 React 컴포넌트로 내려가는 data-flow graph를 만들 수 있다.

atoms는 컴포넌트가 구독할 수 있는 상태의 단위다. Selectors는 atoms 상태 값을 동기 또는 비동기 방식을 통해 변환한다. 

 

쉽게 정의하면 키와 기본 값을 정의하는 atoms을 만들어 useRecoilState로 상태를 업데이트하는 툴인 것.

그리고 selector는 기존의 atom이나 다른 selector로부터 계산된 값을 생성할 수 있게 해 주는 것.

 

솔직히 써봐야 이해하기 쉬운 것 같기는 하다.

 

아래는 Recoil에서 말하는 Recoil의 사용 이유이다.

 

- Recoil은 공유상태도 React의 내부 상태처럼 간단한 get/set 인터페이스로 사용할 수 있도록 boilerplate-free API를 제공한다.

- Recoil은 동시성 모드를 비롯한 다른 새로운 React의 기능들과의 호환 가능성도 갖는다.

- 상태 정의는 점진적이고 분산되어 있기 때문에, 코드 분할이 가능하다.

- 상태를 사용하는 컴포넌트를 수정하지 않고도 상태를 파생된 데이터로 대체할 수 있다.

- 파생된 데이터를 사용하는 컴포넌트를 수정하지 않고도 파생된 데이터는 동기식과 비동기식 간에 이동할 수 있다.

- Recoil은 탐색을 일급 개념으로 취급할 수 있고 심지어 링크에서 상태 전환을 인코딩할 수도 있다.

- 전체 애플리케이션 상태를 하위 호완하는 방식으로 유지하기가 쉬우므로, 유지된 상태는 애플리케션 변경에도 살아남을 수 있다.

 

즉, 다른 상태 관리 툴이 아닌 Recoil을 선택해야 하는 상황은 다음과 같다.

 

1. 복잡한 파생 상태 관리가 필요한 경우

- Recoil의 selector는 여러 상태를 결합하거나 파생 상태를 계산할 때 매우 강력하다. Redux와 같은 라이브러리에서도 파생 상태를 생성할 수 있지만, Recoil의 selector는 기본적으로 캐싱을 지원하여 성능 최적화를 제공하고, 비동기 상태도 쉽게 처리할 수 있다.

 

2. React와의 자연스러운 통합을 선호하는 경우

- Recoil은 React에서 작동하도록 설계된 라이브러리로, React의 상태 관리 방식과 일관된 API를 제공한다.

- 특히 React의 기존 useState와 비슷한 사용성을 원한다면 Recoil은 이를 자연스럽게 확장한 느낌을 준다.

 

3. 동적 상태가 필요할 때

- Recoil의 atomFaimily와 selectorFamily는 파라미터를 통해 상태를 동적으로 생성할 수 있도록 한다.

이는 동적 리스트나 상황에 따라 다른 상태가 필요한 경우 매우 유용하다.

 

4. 비동기 데이터 관리가 많은 경우

- selector는 비동기 함수를 기본으로 지원하므로 API 호출이나 비동기 데이터 처리가 매우 간편하다. Recoil은 별도의 설정 없이 바로 비동기 상태 관리를 구현할 수 있다.

- Redux에서 Redux-saga를 추가로 필요로하는 것과 달리 단독으로 비동기 상태관리를 할 수 있다는 뜻

 

 

* 사용법이나 문법 등은 굳이 추가하지 않겠다.

 

 


 

결론적으로 React와 비슷한 문법으로 자연스럽게 전역 상태 관리를 추가하거나 복잡한 비동기 데이터 관리 등이 필요한 경우에 사용하면 좋은 전역 상태 관리 툴이다. 

 

다른 프로젝트들을 보니 Recoil은 로그인 상태 관리 혹은 전역 테마 상태 (다크 모드) 관리에 유용하게 사용되는 것 같다.

 

나도 후에 다크 모드 기능 구현을 시도해보고 싶었는데 그 때 Recoil을 유용하게 사용할 수 있을 것 같다. 

 

 

 

처음 동아리에 들어가 스터디를 하며 Redux에 배웠고, 실제로 개발을 하며 더 가벼운 상태 관리 도구가 필요하다고 생각해 Recoil을 사용해 봤고, 스타트업에서 알바를 할 때는 Zustand로 상태 관리를 하라고 했다.

 

상태 관리는 다 거기서 거기 아닌가라고 생각이 들었지만, 상태 관리 도구에 따라 기본 원리와 사용법이 다르다.

 

각각의 상태 관리의 특성들을 이해하고 있어야 상황과 환경에 맞춰 알맞은 상태관리 도구들을 선택할 수 있겠다는 생각이 들어 미리 각 도구들에 대해 이해해놓고자 한다.

 

사실 이미 가볍게 한 번 공부한 적이 있으나 좀 대충 공부했나.. 잘 생각이 나지 않는다. 

(그래서 복습 겸 .. 오블완 겸 ..)

 

https://asmallroom.tistory.com/19

 

React (Redux , Context API, 클래스형과 함수형)

리액트의 상태 관리애플리케이션이 커짐에 따라 상태가 어떻게 구성되고 구성 요소 간에 데이터가 어떻게 흐르는 지에 대해 더 의도적으로 생각하는 것은 도움이 된다.중복되거나 중복된 상태

asmallroom.tistory.com

 

좀 더 심화적으로 공부해야겠다.

 

특히 후의 개발 면접 등을 생각해봤을 때 프로젝트에서 특정 스택을 사용한 이유를 설명할 수 있어야 한다고 생각하니 그 중요성이 더 체감이 된다. 

 

오늘은 첫번째 상태 관리 도구인 Redux이다. 

 


Redux

 

https://ko.redux.js.org/introduction/getting-started/

 

Redux 시작하기 | Redux

소개 > 시작하기: Redux를 배우고 사용하기 위한 자료

ko.redux.js.org

 

 

Redux는 내가 처음 접한 상태 관리 도구이다. 하지만 스터디 당시 너무 어렵고 활용하기가 어렵다는 생각이 들어 제대로 이해하지 못했던 도구이기도 하다.

 

Redux란 뭘까?

 

- 자바스크립트 앱을 위한 상태 관리 애플리케이션으로, Redux는 React와 함께 자주 사용되지만 리액트에 한정되지 않고 모든 애플리케이션에서 사용할 수 있다.

 

Redux의 핵심 개념

 

- 스토어 : 애플리케이션의 상태를 담고 있는 객체. 단 하나만 존재하며 애플리케이션의 모든 상태가 저장된다.

- 액션 : 상태를 변화시키기 위한 의도를 나타내는 객체

- 리듀서 : 액션을 받아 상태를 업데이트하는 함수이다. 이전 상태와 액션을 인자로 받아 새로운 상태를 반환하는 순수 함수이다.

- 디스패치 : 액션을 스토어에 전달하는 함수이다. 디스패치된 액션은 리듀서를 통해 상태를 업데이트 하게 된다.

 

=> Redux에서 액션을 보내면, Reducer 함수를 작동시켜, 스토어를 업데이트 한다.

 

아래의 움짤을 통해 이 과정을 쉽게 이해할 수 있다. 

 

 

 

 

아래는 이 글을 쓰면서 발견한 칼럼이다. 이 글을 읽고 Redux를 사용해야하는 규모와 이유, 상황에 대해 이해했다.

 

리덕스가 필요한 이유

 

Redux가 필요하다는 것을 언제 알 수 있나요?

이 글은 Simon Schwartz의 "When do I know I’m ready for Redux?"를 번역한 글입니다.

medium.com

 

위의 칼럼에서 말하는 Redux를 사용해야하는 순간은 다음과 같다.

 

- 앱 상태의 형태가 여러 컴포넌트에 걸쳐 퍼져있을 때

- 상태를 바꾸는 함수가 여러 컴포넌트에 걸쳐 퍼져있을 때

- 앱 상태의 개요 파악을 위해 마음속에 앱 구조에 대한 모델을 가지고 있어야 할 때

- 여러 단계의 컴포넌트에 걸쳐 같은 props를 전달해야 할 때

- 디버깅할 때 상태 변경을 추적하기가 힘들어졌을 때

 

사실 전역 상태 관리 도구인 만큼 작거나 간단한 앱은 이런 상황을 이르지 않기 때문에 굳이 상태 관리 앱을 적용해 사용할 필요는 없는 것 같다. 특히 Redux는 너무 장황하고 익히는 데 시간이 걸리기 때문에 급하게 상태 관리를 도입해야 하는 경우에는 선택하고 싶지 않다. 

 

하지만, 기업별 기술 스택을 확인할 수 있는 페이지인 Codenary에 Redux를 검색해보니 많은 기업, 특히 대기업들도 Redux를 많이 사용하는 것을 확인할 수 있었다.

 

실제로 나도 공고들을 보면서 Redux 사용 경험에 대해 써져있는 공고들을 심심찮게 발견하기도 했다.

 

아무래도 기업들의 프로젝트는 사이즈가 크고, 데이터가 많기 때문이다.

관리해야 할 데이터가 많을수록 데이터를 데이터를 엄격하기 관리해야 하기 때문일 것이다.

 

Redux의 공식 홈페이지에서도 다음 처럼 말하고 있다.

 

 

단지 누군가가 사용하라고 했다는 이유만으로 Redux를 사용하지는 마세요. - 시간을 들여서 잠재적인 이점과 그에 따르는 단점을 이해하세요.  

 

그럼 Redux는 언제 쓰란거지.. 웬만한 상태 관리는 다른 쉬운 상태 관리 툴들로 커버가 될 것 같은데 왜 다들 쓰고 있는 걸까.

공식에서 제시하는 Redux가 사용되어야 하는 상황은 다음과 같다.

 

- 계속해서 바뀌는 상당한 양의 데이터가 있다

- 상태를 위한 단 하나의 근원이 필요하다

- 최상위 컴포넌트가 모든 상태를 가지고 있는 것은 더 이상 적절하지 않다

 

 


 

어려워요 언제 써야되는 지 몰겠어요

 

 

다만 확실한 거는 제가 해왔던 자그마한 플젝들 중에서는 Redux가 쓰일만한 복잡한 상태 관리도 상태가 왔다갔다해야하는 트리구조도 없었기 때문에 딱히 도입할 필요가 없었다는 것을 느꼈다.

 

근데 어떤 방면에서 보면 Redux가 필요하지 않은 가벼운 프로젝트들만 진행한 것이기 때문에 복잡한 로직 구조와 데이터의 상태 분리 경험도 못 해본 것이기 때문에 약간의 아쉬움이 느껴진다.

 

이후 사용할 일이 있거나 더 배운 것이 있어서 이 글이 수정되기를 ...🙏

 

useReducer

  • useReducer()을 는 상태와 그 상태를 업데이트하는 함수를 반환하는 React 훅입니다. 이 훅은 상태를 업데이트하는 로직을 함수 형태로 분리해 관리한다.
  • useReducer은 기본적으로 Redux에서 사용하는 패턴과 유사하게 동작하며, 상태 변경 로직을 컴포넌트 외부로 분리할 수 있어 상태 관리가 복잡해질 때 매우 유용하다.
const [state, dispatch] = useReducer(reducer, initialState);
  • reducer
    • 현재 상태와 액션을 기반으로 새로운 상태를 반환하는 함수
  • initialState
    • 초기 상태 값
  • state
    • 현재 상태를 나타낸다.
  • dispatch
    • 상태를 변경하기 위한 액션을 발생시키는 함수

useReducer의 역할

  • 복잡한 상태 관리
    • useState를 사용하면 여러 상태 변수를 개별적으로 관리해야 하는 경우가 많은데, useReducer을 사용하면 상태와 그 변화를 하나의 함수에서 관리할 수 있어 복잡한 상태 로직을 쉽게 다룰 수 있다.
  • 코드 가독성 향상
    • 상태 변화를 관리하는 로직을 reducer 함수로 분리하기 때문에 코드가 더 깔끔하고 가독성이 높아진다.
  • 유지보수성 개선
    • 상태 변경 로직이 한 곳에 모이므로, 유지보수와 테스트가 용이하다.

useState ⇒ useReducer

useState

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

export default Counter;

useReducer

import React, { useReducer } from 'react';

// reducer 함수 정의
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Counter() {
  const initialState = { count: 0 };
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
    </div>
  );
}

export default Counter;
  • reducer 함수는 현재 상태와 액션을 인자로 받아, 새로운 상태를 반환한다.
  • dispatch 함수를 사용해 액션을 발생시키고, 이를 통해 상태를 변경한다.

useReducer 사용

  • 복잡한 상태 로직이 필요한 경우
    • 상태 업데이트 로직이 여러 가지 경우를 처리해야 할 때 useState가 더 적합하다.
    • 여러 상태 변수나 다양한 액션 타입이 필요한 경우
  • 상태 관리의 일관성
    • 상태 변경 로직을 하나의 reducer 함수에 통합하면, 코드의 일관성을 유지하고 버그를 줄일 수 있다.

useReducer 객체 관리

import React, { useReducer } from 'react';

// reducer 함수
function reducer(state, action) {
  switch (action.type) {
    case 'setName':
      return { ...state, name: action.payload };
    case 'setAge':
      return { ...state, age: action.payload };
    case 'toggleIsStudent':
      return { ...state, isStudent: !state.isStudent };
    default:
      throw new Error('Unknown action');
  }
}

function UserProfile() {
  const initialState = { name: '', age: 0, isStudent: false };
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Name: {state.name}</p>
      <p>Age: {state.age}</p>
      <p>Is Student: {state.isStudent ? 'Yes' : 'No'}</p>

      <input
        type="text"
        placeholder="Set Name"
        onChange={(e) => dispatch({ type: 'setName', payload: e.target.value })}
      />
      <input
        type="number"
        placeholder="Set Age"
        onChange={(e) => dispatch({ type: 'setAge', payload: parseInt(e.target.value) })}
      />
      <button onClick={() => dispatch({ type: 'toggleIsStudent' })}>
        Toggle Is Student
      </button>
    </div>
  );
}

export default UserProfile;
  • reducer를 통해 객체 안의 요소를 상태 관리할 수 있다.

결론

  • useReducer은 useState보다 더 복잡한 상태 관리가 필요할 때 유용한 도구이다.
  • 상태 업데이트 로직이 복잡해지거나 다양한 경우를 처리해야 한다면, useReducer로 전환하여 관리할 수 있다.

 

 

 

- JS, 리액트를 사용한 프로젝트 진행 중 막대창으로 인한 불편함이나 웹사이트를 들어가야하는 불편함이 크게 느껴져서 PWA로 만들기로 했다. 

- 막 어려웠던 게 아니긴 한데... PWA 개념도 모르다가 한 번에 적용까지 하려니 삽질을 좀 했다

 

 


PWA란 ?!

Progressive Web App. 쉽게 말해 웹 앱이다. 웹페이지를 그대로 '앱'화해서 방문과 공유를 쉽게 만든다.

이미 만든 웹사이트를 그대로 모바일에서 앱처럼 동작하게 만들 수 있어서 좋다.

이미 트위터, 스타벅스, 핀터레스트 등은 PWA를 사용하고 있다.

 

중요한 점

PWA를 만들기 위해서 중요한 개념인 service-worker (브라우저와 네트워크 중간에 있는 가상 프록시를 의미한다)는 안전한 컨텍스트, 즉 HTTPS 주소에서만 실행된다는 점이다. HTTPS를 가진 주소가 있으면 가장 좋을 것 같고, PWA를 위해 코드를 수정하면서 실험해 보니 localhost에서도 테스트가 가능한 것 같다. 


 

PWA 만드는 법 정리 

1. manifest.json 파일을 정의한다.

가장 맨 아래에 있는 manifest.json 파일.

기존 리액트 프로젝트 생성 시 필요 없는 파일이기 때문에 manifest.json 파일을 삭제해 없는 경우가 많은데, PWA 파일을 만들기 위해서는 반드시 필요하니 다시 manifest.json 파일을 public 파일 안에 생성해 넣어준다. 파일의 내용은 아래의 PWA Builder 에서 쉽게 생성 할 수 있다. PWA Builder는 PWA를 쉽게 적용할 수 있도록 도와주는 도구이니 끄지 말고 한 구석에 계속 켜 놓기를 추천한다. 

 

https://www.pwabuilder.com/

 

여기서 'Enter the URL to your PWA'에 주소를 넣으면 (로컬이든 배포 주소든) 

- manifest.json 파일이 있을 시 보통 아래처럼 내 주소가 얼마나 PWA에 적합한 지를 알려주며 점수를 매겨준다.

 (지금 나의 점수는 보다시피 딱히 높지는 않다..ㅎㅎ;)

- manifest.json 파일이 없을 시 아마 찾을 수 없다며 manifest.json 파일을 수정하거나 추가하라며 알려줄 것이다. 

 

 

 

Edit your Manifest 파일에서 보이는 요소들. 

name, short name, id, description 등 프로젝트에 맞춰서 후루룩 쓴다. 어차피 파일에서 바로바로 수정할 수 있으니까 엄청 신중할 필요는 없다. Settings에서 있는 요소들도 맞춰서 변경해준다. StartURL은 홈 화면, 웹의 시작 주소를 말한 거니까 보통은 / 그대로 놔두면 된다. Language도 알아서 맞춰준다. Display는 화면이 어떤 방식으로 보이느냐인데 보통 standalone 혹은 fullscreen으로 많이 쓰는 것 같다. 나는 위에 도구 막대 빼고 탭은 있는 게 안정적으로 보이는 것 같아서 standalone을 선택했다. 

 

 

그리고 아이콘 만들기가 나오는데... 우리 프로젝트는 로고도 없고 상징적인 아이콘도 아직 없어서 내가 급하게 하나 그렸다;; 나중에 제대로 갈아끼워야지 여기서 하나 그려서 사이즈별로 만들어서 넣으면 favIcon, 모든 기종/화면에서 앱 사용 시 보이는 이미지를 퉁칠 수 있다. 그래서 가급적 예쁘고 귀여울수록 좋은 것 같다 .... 512x512 이상 이미지를 넣으면 알아서 크기별로 만들어주고 파일로 압축해준다. 이 파일들 역시 public/icons/ 파일 안에 모두 넣어주면 된다. ( 꼭 이 주소는 아니여도 된다)

 

이렇게 manifest.json 파일이 완성되면 파일 내용을 넣어주고 index.html에서 manifest.json 파일을 찾을 수 있게 주소도 제대로 적어준다.

 

**

만들어진 아이콘들을 manifest와 index에 제대로 넣어줘야 하는 것 같다!

 

2. offline.html 페이지 만들기

 

루트 아래에 offline.html 페이지를 만들고 바르지 않은 주소로 간 경우 보여 줄 화면을 구현한다. 

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>인터넷 연결 없음</title>
  </head>
  <body>
    <p>인터넷 연결 없음</p>
  </body>
</html>

 

3. dependency 추가

        "workbox-background-sync": "^5.1.4",
        "workbox-broadcast-update": "^5.1.4",
        "workbox-cacheable-response": "^5.1.4",
        "workbox-core": "^5.1.4",
        "workbox-expiration": "^5.1.4",
        "workbox-google-analytics": "^5.1.4",
        "workbox-navigation-preload": "^5.1.4",
        "workbox-precaching": "^5.1.4",
        "workbox-range-requests": "^5.1.4",
        "workbox-routing": "^5.1.4",
        "workbox-strategies": "^5.1.4",
        "workbox-streams": "^5.1.4"

 

4. service-worker.js 파일 && serviceWorkerRegistration.js 파일 추가

역시 루트 아래에 두 파일을 추가해준다. 

service-worker.js

import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';

// 클라이언트 클레임
clientsClaim();

// 프리캐시 및 라우팅 설정
precacheAndRoute(self.__WB_MANIFEST);

const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
registerRoute(
  ({ request, url }) => {
    if (request.mode !== 'navigate') {
      return false;
    }

    if (url.pathname.startsWith('/_')) {
      return false;
    }
    if (url.pathname.match(fileExtensionRegexp)) {
      return false;
    }

    return true;
  },
  createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html'),
);

const offlineFallbackPage = 'offline.html';

// 이미지 캐싱
registerRoute(
  ({ url }) =>
    url.origin === self.location.origin && url.pathname.endsWith('.png'),
  new StaleWhileRevalidate({
    cacheName: 'images',
    plugins: [new ExpirationPlugin({ maxEntries: 50 })],
  }),
);

// 서비스 워커 업데이트 메시지 처리
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

 

serviceWorkerRegistration.js

 


const isLocalhost = Boolean(
  window.location.hostname === 'localhost' ||
    window.location.hostname === '[::1]' ||
    window.location.hostname.match(
      /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
    ),
);

export function register(config) {
  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
    const publicUrl = new URL(PUBLIC_URL, window.location.href);
    if (publicUrl.origin !== window.location.origin) {
      return;
    }

    window.addEventListener('load', () => {
      const swUrl = `${PUBLIC_URL}/service/service-worker.js`;

      if (isLocalhost) {
        checkValidServiceWorker(swUrl, config);

        navigator.serviceWorker.ready.then(() => {
          console.log(
            'This web app is being served cache-first by a service ' +
              'worker. To learn more, visit https://cra.link/PWA',
          );
        });
      } else {
        registerValidSW(swUrl, config);
      }
    });
  }
}

function registerValidSW(swUrl, config) {
  navigator.serviceWorker
    .register(swUrl)
    .then((registration) => {
      registration.onupdatefound = () => {
        const installingWorker = registration.installing;
        if (installingWorker == null) {
          return;
        }
        installingWorker.onstatechange = () => {
          if (installingWorker.state === 'installed') {
            if (navigator.serviceWorker.controller) {
              console.log(
                'New content is available and will be used when all ' +
                  'tabs for this page are closed. See https://cra.link/PWA.',
              );

              // 성공 콜백 실행
              if (config && config.onUpdate) {
                config.onUpdate(registration);
              }
            } else {
              console.log('Content is cached for offline use.');

              if (config && config.onSuccess) {
                config.onSuccess(registration);
              }
            }
          }
        };
      };
    })
    .catch((error) => {
      console.error('Error during service worker registration:', error);
    });
}

function checkValidServiceWorker(swUrl, config) {
  fetch(swUrl, {
    headers: { 'Service-Worker': 'script' },
  })
    .then((response) => {
      const contentType = response.headers.get('content-type');
      if (
        response.status === 404 ||
        (contentType != null && contentType.indexOf('javascript') === -1)
      ) {
        navigator.serviceWorker.ready.then((registration) => {
          registration.unregister().then(() => {
            window.location.reload();
          });
        });
      } else {
        registerValidSW(swUrl, config);
      }
    })
    .catch(() => {
      console.log(
        'No internet connection found. App is running in offline mode.',
      );
    });
}

export function unregister() {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker
      .register('/service-worker.js')
      .then((registration) => {
        console.log(
          '서비스 워커가 다음 범위로 등록되었습니다:',
          registration.scope,
        );
      })
      .catch((error) => {
        console.error('서비스 워커 등록에 실패했습니다:', error);
      });
  } else {
    console.log('이 브라우저에서는 서비스 워커를 지원하지 않습니다.');
  }
}

 

5. 계속해서 light house와 PWA builder를 확인한다.

이렇게 깜찍한 앱으로 다운 받을 수 있을 때까지 시도하면 된당

+ Recent posts