• 개요

본 문서는 PWA ( progressive web app )에 대해 조사한 문서이다. 기본 개념, 리액트 웹에 적용하는 방법 등에 대해 설명한다. 

 

  • PWA란? 

PWA는 우리가 웹 브라우저로 접속해서 보는 웹 기반 기술로 만든 ‘앱’이다. 웹 기술로 앱을 만드는 방법은 다양하게 연구되고 있다. 우리에게 친숙한 HTML,CSS,JS, React 등으로 구현가능하고, 심지어 기존 웹을 웹인걸 눈치채지 못하게 앱처럼 만들 수 있는 기능이다. 즉, 네이티브 앱 개발없이 앱을 빠르게 개발할 수 있다는 장점이 있다. 또한, 푸시 알림이나 오프라인 지원 같은 네이티브 앱만의 장점을 전부 제공할 수 있다.

 

  • 사례

우리가 많이 사용하는 사이트 중에도 PWA를 많이 찾아볼 수 있다. 본인도 구글 챗 등의 웹 앱을 PWA로 추가해서 데스크톱앱으로 사용하고 있다. 또, 트위터가 대표적이다. 트위터 사이트에 접속하면 홈화면에 트위터앱을 추가할 수 있다. 웹 사이트 북마크를 저장하는 것처럼 보이지만, 브라우저 창은 숨겨져 앱과 거의 동일한 사용경험을 제공한다. 또, os에 상관없이 한번의 개발로 동일한 경험을 제공한다. 

구글 앱들, 스타벅스, 핀터레스트, 워싱턴포스트, 우버 등도 PWA를 지원한다.

 

  • 네이티브 vs PWA

 

네이티브 앱은 보통 전용 앱으로 개발한다. 요즘은 Flutter와 같은 멀티 플랫폼 언어도 등장하고 있지만, 이 들도 네이티브 앱에 대해 어느정도 알아야 개발할 수 있다. 그러나 PWA는 웹 개발자가 쉽게 앱 개발 (정확히는 앱 사용 경험을 주는 웹앱 ) 을 할 수 있다.

 

  • 장점

 

  • 앱스토어 출시를 위한 비용이 발생하지 않는다.
  • 설치 과정이 필요 없다
  • 개발 비용이 저렴하다.
  • 반응형 웹일 경우 이는 앱에서도 잘 작동한다.
  • 부드럽고 가볍다.
  • 일반적인 웹사이트와 달리 오프라인에서도 동작한다.
  • 검색엔진에 노출된다.
  • 푸시 기능 사용이 가능하다.
  • 구성 요소

PWA를 구성하기 위해서는 크게 3가지 작업이 필요하다.

  • https or localhost
  • service worker
  • manifest.json
  • 적용

 

현재 본인이 리액트로 개발중인 솔루션에 PWA를 간단하게 적용해보았다. 앞서 말한 3가지를 잘 설정하면 어렵지 않게 적용할 수 있다. localhost 환경이기때문에 1번은 생략한다. 

  • service worker 등록
    •  npx create-react-app my-app --template cra-template-pwa
    • 위 코드를 통해 pwa 신규 프로젝트를 생성 후 서비스를 새로 개발하거나, 기존 프로젝트의 root 폴더에 service-worker.js 파일과 serviceWorkerRegistration.js 파일을 그대로 가져와 사용도 가능하다. 직접 서비스 워커 파일을 만들어 사용해도 되지만, 똑똑한 분들이 미리 만들어둔 파일을 사용하면 편리하다. 파일의 코드 내용은 생략한다.

 

  • 필요한 모듈을 설치한다.
  • manifest.json 파일 설정한다.
  • 프로젝트 실행 및 PWA 적용
  1. “yarn add ~” or “npm install ~”
  2. React 프로젝트를 빌드 후 serve 한다. ( npx serve -s build ) 
  3. yarn start / npm start 등의 개발 모드의 실행으로는 PWA를 설정할 수 없다. 반드시 빌드 후 production 모드로 serve한 웹으로 접속해야 활성 가능하다.
  4. 웹에 접속 후 개발자도구 – lighthouse – pwa 체크 후 검증
  5. 아래와 같은 축하 메세지를 받으면 PWA 적용이 완료된 것이다. ( 적용이 가능한 사이트로 인정받은 것이다.)
  6.  
  7. 웹 사이트의 주소창 옆 버튼을 순차로 클릭하면 PWA 웹앱을 데스크톱 앱처럼 활성이 된 것을 확인할 수 있다.
  8. 모바일에서는 아이폰 기준 홈화면에 추가를 누르면 홈화면에 앱 아이콘이 추가되고, 이를 실행하면 주소창 없는 PWA를  실행 가능하다. 



  • Next Plan

본 문서는 PWA의 개요와 기본 적용만 다루었다. 이후 개발을 진행하면서 push 알림과 같은 네이티브 앱의 기능들을 스터디하고 적용해볼 것이다. 네이티브 기능들을 잘 다루고 활용하고 이를 서비스에 잘 활용하면 더욱 좋은 사용자 경험을 만들 수 있다는 기대감과 웹 개발자로서 더 의미 있는 개발을 많이 할 수 있을거라 생각이 든다.



 




 

'Research' 카테고리의 다른 글

AresDB  (0) 2023.04.19
WEB 3.0이란?  (1) 2023.02.17
Jager(예거)에 대해서  (1) 2022.12.26
Git 활용 방법과 브랜치 전략  (1) 2022.11.30
Prometheus(프로메테우스) 개념 및 timescaleDB로 변환  (0) 2022.11.30

JS 배열 및 객체에서 일반적인 깊은 복사 얕은 복사 개념에 대해서는 넘어가고, 

nested된 객체 및 배열에 대해 얘기한다.

 

nested는 "중첩된"이라는 뜻인데,

일반적으로 많이 사용하는 깊은 복사 방법인 스프레드 연산 (...) 를 통해 객체를 복사하면 깊은 복사가 되긴한다.

단, 객체 안 객체, 배열 안 배열은 깊은 복사 되지 않는다.

 

// 깊은 복사
let obj = {a:'a'}
let deepCopy = {...obj}

// nested object
let obj = {a:'a', nested:{b:'b'}}
let deepCopy = {...obj}
let deepCopy2 = {...obj}


deepCopy.nested.b = 'B';
deepCopy2.nested.b = 'C';

console.log(deepCopy.nested.b) // 'C' 
console.log(deepCopy2.nested.b) // 'C'

외부 라이브러리 없이 해결 방법으로는 

let deepCopy = JSON.parse(JSON.stringify(obj));

이 있다.

 

lodash 같은 라이브러리를 활용하면

import cloneDeep from 'lodash/cloneDeep';

let deepCopy = cloneDeep(obj)

도 가능하다.

'Javascript' 카테고리의 다른 글

Promise를 지원하지 않는 NPM 모듈 Promise로 변환  (0) 2023.04.14

Container / Presentation 방식

container : 데이터 조작을 다루는 컴포넌트

presentation : 화면을 다루는 컴포넌트

CRA는 NPX로

NPX는 그 순간 최신의  소스를 받아와서 설치 후 삭제함

NPM 전역으로 CRA를 설치후 CRA를 하면, 설치 당시의 버전으로 사용해야하고,

나중에 패키지가 업데이트 되면 새로 전역에 설치해야함

또, CRA의 의존성 패키지들을 로컬에 남겨두지 않음

React + type script

npx create-react-app my-app --template typescript

React에 typescript , scss 추가

yarn add -D typescript @types/node @types/react @types/react-dom @types/jest
yarn add sass

// tsconfig.js
{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": false,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
  },
  "include": [
    "src"
  ]
}

React + eslint + prettier + redux tool-kit

yarn add -D prettier eslint-config-prettier eslint-plugin-prettier eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
yarn add @reduxjs/toolkit react-redux react-router-dom

// .eslintrc.js
module.exports = {
  env: {
    browser: true,
    node: true,
  },
  extends: [
    'prettier',
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 12,
    sourceType: 'module',
  },
  plugins: ['react', '@typescript-eslint'],
  rules: {
    'prettier/prettier': ['error', { endOfLine: 'auto' }],
  },
};

// .prettierrc.js
module.exports = {       
  // String is in single quotes (') 
  // 문자열은 홀따옴표(')로 
  singleQuote: true,
  // With a semicolon at the end of the code. 
  // 코드 마지막에 세미콜른이 있게 
  semi: true,
  // Do not use tabs and replace them with space bars. 
  // 탭의 사용을 금하고 스페이스바 사용으로 대체하게 
  useTabs: false,
  // Indentation width of 2 spaces 
  // 들여쓰기 너비는 2칸
  tabWidth: 2,
  // When you create an object or array, you also put a comma on the element or on the back of the key-value.
  // 객체나 배열을 작성 할 때, 원소 혹은 key-valueㅇ의 맨 뒤에 있는 것에도 쉼표를 붙임
  trailingComma: 'all',
  //One line of code is maximum 80 spaces 
  // 코드 한줄이 maximum 80칸
  printWidth: 80,
};

filter vs find

const item = productList.find((list: productListProps) => {
    return list.id === Number(id);
});
// 조건에 해당하는 요소 반환
const item2 = productList.filter((list: productListProps) => {  //
  return list.id === Number(id);
});
// 배열만들어서 만들어줌

React Query

yarn add react-query

//index.tsx
import { QueryClient, QueryClientProvider } from 'react-query';
const queryClient = new QueryClient();

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement,
);
root.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <Provider store={store}>
        <BrowserRouter>
          <App />
        </BrowserRouter>
      </Provider>
    </QueryClientProvider>
  </React.StrictMode>,
);


// DashBoard.tsx 대쉬보드 컴포넌트
import axios from 'axios';
import { useQuery } from 'react-query';
import * as React from 'react';

const DashBoard = () => {
   const result = useQuery(
    '작명',
    () =>
      axios.get('https://codingapple1.github.io/userdata.json').then((a) => {
        return a.data;
      }),
    {
      refetchOnWindowFocus: false, // react-query는 사용자가 사용하는 윈도우가 다른 곳을 갔다가 다시 화면으로 돌아오면 이 함수를 재실행합니다. 그 재실행 여부 옵션 입니다.
      retry: 0, // 실패시 재호출 몇번 할지
      onSuccess: (data) => {
        // 성공시 호출
        console.log(data);
      },
      onError: (e) => {
        // 실패시 호출 (401, 404 같은 error가 아니라 정말 api 호출이 실패한 경우만 호출됩니다.)
        // 강제로 에러 발생시키려면 api단에서 throw Error 날립니다. (참조: https://react-query.tanstack.com/guides/query-functions#usage-with-fetch-and-other-clients-that-do-not-throw-by-default)
        console.log(e.message);
      },
    },
  );

  return (
    <div>
      {result.isLoading && '로딩중'}
      {result.error && '에러남'}
      {result.data && result.data.name}
    </div>
  );
};

export default DashBoard;

불변성 지키면서 배열 업데이트

let [users, setUsers] = useState([]);

# 배열에 추가
setUsers(users.concat(user));
# 배열에 추가 (함수형)
setUsers((prev) => prev.concat(user));

# 배열에서 삭제 
const onRemove = id => {
  // user.id 가 id 인 것을 제거
  setUsers(users.filter(user => user.id !== id));
};
 
#배열 수정
const onToggle = id => {
  setUsers(
    users.map(user =>
      user.id === id ? { ...user, active: !user.active } : user
    )
  );
};

불변성 지키면서 객체(object) 업데이트

#객체에 추가 #객체 업데이트
setUsers(state => {...state, key: value})

#객체에서 제거 #loadash
setUsers(state => {..._.omit(state, 'deleteKey')})

 

맨 아래로 Scroll 하기

messageListRef.current?.scrollTo(0, messageListRef.current.scrollHeight);

필자가 접한 react 튜토리얼을 보면

리액트의 프톤트에서 백엔드로 요청을 할때

중간에  proxy서버를 두고 요청한다.

"proxy": "http://localhost:7080",   //package.json

처음에는 단순히 리액트는 3000에 실행되어있고

서버의 경우 7080포트(필자의 백엔드 서버)에 실행되어있으니

CORS( cross-origin requests)를 방지하기 위함인줄 알고 개발을 진행했다. 

 cors는 웹개발시 무조건 만나는 기초 내용이니 검색해보자!

 

* 간단하게 추가 설명을 보태면 3000에서 실행된 리액트가 보낸 요청을 프록시 서버인 7080이 가로챈 뒤 백엔드서버인 7080으로 요청을 보내기 때문에 CORS가 발생하지 않는다.

 

 

허나 한가지 이유가 더있다.

axios.get('http://localhost:3001/users?email=${email}')
    .then(res => {
        console.log(res);
    }
         
fetch('http://localhost:3001/auth/login',{
    method:"post",
    body : JSON.stringify(loginInfo),
})

proxy 설정을 하지 않고 ajax를 요청하면 위와 같이 코딩이  될 것이다.

프론트에서 요청하는 부분이 한부분도 아니고 만약 주소가 바뀌거나 

서비스를 실제 디플로이한다면 고객사마다 주소가 다르고 포트가 다르고 등등의 상황이 생길 것이다.

그러면 react 프로젝트 내 모든 ajax 부분의 주소를 다 바꾸어야한다.

유지보수가 똥망이 된다.

 

axios.get('/users?email=${email}')
    .then(res => {
        console.log(res);
    }
         
fetch('/auth/login',{
    method:"post",
    body : JSON.stringify(loginInfo),
 })
"proxy": "http://localhost:7080",   //package.json

이를 위와 같이 수정하고 package.json에 프록시를 추가한다.

처음 코드와 똑같이 작동할 것이다.

 

위와 같이 하면 proxy부분만 수정하면 된다.

 

 

 

 

일반적인 컴포넌트 렌더링으로 변수(props)를 넘길때

<DashboardView param1={parameter}/>

 이런식으로 넘긴다.

<컴포넌트이름 파라미터이름={값}> 과 같은 형태 !!

그런데 Router 를통해 라우팅할때는 

<Route path="/admin/DashboardView" exact component={DashboardView} />

이런 형태로 component를 호출한다.
그럼 변수는 어떻게 넘긴단 말인가?

<Route path="/admin/DashboardView" exact component={() => <DashboardView param1={parameter} />}/>

이런식으로 넘기면 넘길 수 있다.

그리고 해당 컴포넌트에서는 

// DahboardVeiw.js
export default function DashboardView(props) {
	console.log(props.param1);
	render(<div>리액트 배워보자</div>)
}

이와 같이 기존 props를 전달받듯이 받으면 된다.

 

+ Recent posts