230609 - React, PWA(+ service-worker)

๋ฐฑ์Šน์—ฐยท2023๋…„ 6์›” 9์ผ
1

๐Ÿšฉ React

firebase ์ด์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ ์ €์žฅ(๋ฏธ์™„์„ฑ)

๐Ÿ“ ์„ค๋ช…

  • firebase๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‚ด์šฉ์„ ์ž…๋ ฅ๋ฐ›์•„ ์ถœ๋ ฅํ•  ์ˆ˜ ์žˆ๋„๋ก ์ฝ”๋“œ ์ˆ˜์ • ์˜ˆ์ •


โœ’๏ธ ์‚ฌ์šฉ๋ฒ•

์ž…๋ ฅ

App.js

import React, { useCallback, useEffect, useState } from "react";
import "./App.scss";
import MovieList from "./components/MovieList";
import AddMovie from "./components/AddMovie";

function App() {
  const [movies, setMovies] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null); // ์—๋Ÿฌ ์—†๋Š” ์ƒํƒœ

  /*
  // ๋น„๋™๊ธฐโญ•
  function fetchMovieHandler() {
    fetch("https://swapi.dev/api/films/")
      .then((response) => {
        return response.json(); // json ๋Œ€์‹  ์šฐ๋ฆฌ๊ฐ€ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜
      })
      .then((data) => {
        const transformedMovies = data.results.map((movieData) => {
          return {
            id: movieData.episode_id,
            title: movieData.title,
            openingText: movieData.opening_crawl,
            releaseDate: movieData.release_date,
            // ํ•„์š”ํ•œ 4๊ฐœ๋งŒ ์ƒˆ๋กœ์šด ๊ฐ์ฒด๋กœ ๋ฐ˜ํ™˜
          };
        });

        setMovies(transformedMovies);
      });
  }
  */

  // ๋น„๋™๊ธฐโŒ (async, await)
  /*
  async function fetchMovieHandler() {
    setIsLoading(true);
    setError(null);

    try {
      const response = await fetch("https://swapi.dev/api/films/");

      if (!response.ok) {
        throw new Error("์—๋Ÿฌ ๋ฐœ์ƒ"); // ๋’ท๋ถ€๋ถ„ ์ฝ”๋“œ ์ง„ํ–‰x
      }

      const data = await response.json();
      const transformedMovies = data.results.map((movieData) => {
        return {
          id: movieData.episode_id,
          title: movieData.title,
          openingText: movieData.opening_crawl,
          releaseDate: movieData.release_date,
          // ํ•„์š”ํ•œ 4๊ฐœ๋งŒ ์ƒˆ๋กœ์šด ๊ฐ์ฒด๋กœ ๋ฐ˜ํ™˜
        };
      });
      setMovies(transformedMovies);

    } catch(error) {
      setError(error.message);
    }
    setIsLoading(false);
  }
  */

  // const ํ˜•์‹์œผ๋กœ ๋ณ€๊ฒฝ
  const fetchMovieHandler = useCallback(async () => {
    setIsLoading(true);
    setError(null);

    try {
      const response = await fetch(
        "https://movietest-34408-default-rtdb.asia-southeast1.firebasedatabase.app/movie.json"
      );

      if (!response.ok) {
        throw new Error("์—๋Ÿฌ ๋ฐœ์ƒ"); // ๋’ท๋ถ€๋ถ„ ์ฝ”๋“œ ์ง„ํ–‰x
      }

      const data = await response.json();
      const transformedMovies = data.results.map((movieData) => {
        return {
          id: movieData.episode_id,
          title: movieData.title,
          openingText: movieData.opening_crawl,
          releaseDate: movieData.release_date,
          // ํ•„์š”ํ•œ 4๊ฐœ๋งŒ ์ƒˆ๋กœ์šด ๊ฐ์ฒด๋กœ ๋ฐ˜ํ™˜
        };
      });
      setMovies(transformedMovies);
    } catch (error) {
      setError(error.message);
    }
    setIsLoading(false);
  }, []); // [] - ํ™”๋ฉด ์—ด๋ฆด ๋•Œ ์ฒ˜์Œ์—๋งŒ ์ž‘๋™๋จ

  // fetchMovieHandler ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋  ๋•Œ๋„ ๋ฐœ์ƒ.
  // ํ•จ์ˆ˜๋Š” ์™ธ๋ถ€์—์„œ ๋ฐ”๋€” ์ˆ˜ ์žˆ์Œ(ex. ๋ณ€์ˆ˜ ๋ฐ›์•„์˜ด) -> ๋ฌดํ•œ๋ฃจํ”„ ๊ฐ€๋Šฅ์„ฑ ์žˆ์Œ
  useEffect(() => {
    fetchMovieHandler();
  }, [fetchMovieHandler]);
  // useCallback(()=>{}, [])

  // ์ถ”๊ฐ€ํ•˜๋Š” ํ•จ์ˆ˜
  const addMovieHandler = (item) => {
    // console.log(item);
    fetch(
      "https://movietest-34408-default-rtdb.asia-southeast1.firebasedatabase.app/movie.json",
      {
        method: "POST", // firebase์— ๋ฆฌ์†Œ์Šค ๋งŒ๋“ฆ
        body: JSON.stringify(item), // js ๊ฐ์ฒด๋ฅผ json์œผ๋กœ ๋ณ€ํ™˜
        // headers: { "Content-Type": "application/json" }, // firebase์—์„œ๋Š” ์ƒ๋žต ๊ฐ€๋Šฅ, API์—์„œ ์–ด๋–ค ์ปจํ…์ธ ๊ฐ€ ์ „๋‹ฌ๋˜๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์คŒ
      }
    );
  };

  let content = <p>NO MOVIES</p>;
  if (isLoading) {
    content = <p>LOADING...</p>;
  }
  if (movies.length !== 0) {
    content = <MovieList movie={movies} />;
  }
  if (error) {
    content = <p>{error}</p>;
  }

  return (
    <main>
      <section>
        <AddMovie onAddMovie={addMovieHandler} />
      </section>

      <section>
        <button onClick={fetchMovieHandler}>Fetch Movie</button>
      </section>

      <section>
        {content}
        {/* isLoading์ด false์ผ ๋•Œ
        {!isLoading && <MovieList movie={movies} />}
        {!isLoading && movies.length === 0 && <p>NO MOVIES</p>}
        {isLoading && <p>LOADING...</p>} */}
      </section>
    </main>
  );
}

export default App;



MovieList.jsx

import React from "react";
import Movie from "./Movie";

const MovieList = (props) => {
  return (
    // map()์„ ์‚ฌ์šฉํ•˜์—ฌ ํ™”๋ฉด์— ์ถœ๋ ฅ
    <ul>
      {props.movie.map((item) => (
        <Movie
          key={item.id}
          id={item.id}
          title={item.title}
          releaseDate={item.releaseDate}
          openingText={item.openingText}
        />
      ))}
    </ul>
  );
};

export default MovieList;



Movie.jsx

import React from "react";

const Movie = (props) => {
  return (
    <li className="movie">
      <h2>{props.title}</h2>
      <h3>{props.releaseDate}</h3>
      <p>{props.openingText}</p>
    </li>
  );
};

export default Movie;



AddMovie.jsx

import React, { useRef } from "react";

const AddMovie = (props) => {
  const titleRef = useRef("");
  const textRef = useRef("");
  const dataRef = useRef("");

  const submitHandler = (e) => {
    e.preventDefault();

    const movie = {
      title: titleRef.current.value,
      openingText: textRef.current.value,
      releaseDate: dataRef.current.value,
    };
    props.onAddMovie(movie);
  };

  return (
    <form onSubmit={submitHandler}>
      <div className="control">
        <label htmlFor="title">title</label>
        <input type="text" id="title" ref={titleRef} />
      </div>

      <div className="control">
        <label htmlFor="text">text</label>
        <textarea id="text" rows="5" ref={textRef}></textarea>
      </div>

      <div className="control">
        <label htmlFor="date">date</label>
        <input type="text" id="date" ref={dataRef} />
      </div>
      <button>Add Movie</button>
    </form>
  );
};

export default AddMovie;



์ถœ๋ ฅ

  • ์ด๋ฏธ์ง€๋กœ ๋Œ€์ฒด


๐Ÿ”— ์ฐธ๊ณ  ๋งํฌ & ๋„์›€์ด ๋˜๋Š” ๋งํฌ






๐Ÿšฉ PWA & Service Worker API

Progressive Web Apps

๐Ÿ“ ์„ค๋ช…

  • ์›น๊ณผ ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ์˜ ๊ธฐ๋Šฅ ๋ชจ๋‘์˜ ์ด์ ์„ ๊ฐ–๋„๋ก ์ˆ˜ ๋งŽ์€ ํŠน์ • ๊ธฐ์ˆ ๊ณผ ํ‘œ์ค€ ํŒจํ„ด์„ ์‚ฌ์šฉํ•ด ๊ฐœ๋ฐœ๋œ ์›น ์•ฑ

Service Worker API

๐Ÿ“ ์„ค๋ช…

  • ์›น ์„œ๋น„์Šค์—์„œ๋„ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ๋™๊ธฐํ™”, ํ‘ธ์‹œ ์•Œ๋ฆผ ๋“ฑ์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ์ง€์›ํ•ด์ฃผ๋Š” ๋„๊ตฌ
  • ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์‹คํ–‰ํ•˜๋Š” ์Šคํฌ๋ฆฝํŠธ
  • ์›นํŽ˜์ด์ง€์™€ ๋ณ„๊ฐœ๋กœ ์ž‘๋™
  • ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ํŒŒ์ผ์˜ ํ˜•ํƒœ๋ฅผ ๊ฐ€์ง
  • ์บ์‹œ ์ €์žฅ
  • ์ด๋ฏธ์ง€๋Š” 192, 512px

### โœ’๏ธ ์‚ฌ์šฉ๋ฒ• jsํŒŒ์ผ์„ ๋งŒ๋“ค์–ด์„œ index์— ์ ์šฉ์‹œํ‚จ๋‹ค.

npm์œผ๋กœ ์„ค์น˜ ๋œ pwa๋Š” npm run dev๋กœ ์‹คํ–‰



sw-precache๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์บ์‹œ ์ €์žฅํ•˜๊ธฐ(์˜คํ”„๋ผ์ธ์—์„œ๋„ ์ ‘์† ๊ฐ€๋Šฅ)

โœ’๏ธ ์‚ฌ์šฉ๋ฒ•

๋ฆฌ์†Œ์Šค์— ์‚ฌ์ „ ์บ์‹œ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“ˆ์„ ๋งŒ๋“ค์–ด ์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

  1. sw-precache-config.js ํŒŒ์ผ ์ƒ์„ฑ or sw-config.js ํŒŒ์ผ ์ƒ์„ฑ
module.exports = {
  staticFileGlobs: [
    "index.html",
    "css/*.css",
    "img/**.*",
    "js/**/*"
  ],
};
  1. ์„ค์ • ์ ์šฉ
    $ sw-precache --config sw-config.js

-> service-worker.js ํŒŒ์ผ์ด ์ƒ๊น€

  • html ๋ฌธ์„œ์—์„œ ์Šคํฌ๋ฆฝํŠธ ๋ถ€๋ถ„ sw.js ๋Œ€์‹  ์ด๊ฑฐ๋กœ ๋ณ€๊ฒฝ

์ž…๋ ฅ

index.html

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <!-- apple -->
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <!-- title -->
    <title>KAKAO - ๋กœ๊ทธ์ธ</title>
    <!-- css -->
    <link rel="stylesheet" href="./css/style.css" />
    <!-- ํŒŒ๋น„์ฝ˜ -->
    <link rel="shortcut icon" href="./img/favicon.png" type="image/x-icon" />
    <!-- manifest -->
    <link rel="manifest" href="manifest.json" />
    <!-- js -->
    <script src="./js/main.js"></script>
    <script>
      if("serviceWorker" in navigator) {
        // ๋ธŒ๋ผ์šฐ์ €์— ์„œ๋น„์Šค์›Œ์ปค๊ฐ€ ์žˆ์„ ๋•Œ (์ง€์›์—ฌ๋ถ€)
        navigator.serviceWorker.register("service-worker.js") // ๋น„๋™๊ธฐ
        .then((success)=>{
          console.log("servieceWorker ์„ค์น˜์— ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค", success);
        })
      }
    </script>
  </head>
  <body>
    <div class="login_container">
      <div class="login">
        <h1 class="login_logo">์นด์นด์˜คํ†ก</h1>

        <form action="./chatList.html" class="login_form">
          <input
            type="text"
            class="login_form_id"
            placeholder="์นด์นด์˜ค๊ณ„์ •(์ด๋ฉ”์ผ ๋˜๋Š” ์ „ํ™”๋ฒˆํ˜ธ)"
          />
          <input type="password" class="login_form_pw" placeholder="๋น„๋ฐ€๋ฒˆํ˜ธ" />
          <input type="submit" class="login_form_btn" value="๋กœ๊ทธ์ธ" />
          <input type="checkbox" class="login_form_check" id="check" />
          <label for="check">์ž๋™๋กœ๊ทธ์ธ</label>
        </form>

        <p class="login_account">
          <a href="">์นด์นด์˜ค ๊ณ„์ • ์ฐพ๊ธฐ</a>
          <span>|</span>
          <a href="">๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •</a>
        </p>
      </div>
    </div>
  </body>
</html>



sw-config.js

module.exports = {
  staticFileGlobs: [
    "index.html",
    "css/*.css",
    "img/**.*",
    "js/**.*"
  ],
};



์ถœ๋ ฅ

  • ์ด๋ฏธ์ง€๋กœ ๋Œ€์ฒด

๐Ÿ”— ์ฐธ๊ณ  ๋งํฌ & ๋„์›€์ด ๋˜๋Š” ๋งํฌ






์ž‘์„ฑ์ค‘

profile
๊ณต๋ถ€ํ•˜๋Š” ๋ฒจ๋กœ๊ทธ

0๊ฐœ์˜ ๋Œ“๊ธ€