๐Ÿคฏ React๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฒซ ๋ฒˆ์งธ Neuro ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌ์ถ•

47226 ๋‹จ์–ด reactwebdevtutorialjavascript
์˜ค๋Š˜๋‚  ๋Œ€๋ถ€๋ถ„์˜ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์€ ์‚ฌ์šฉ์ž์˜ ์˜๋„์— ๋”ฐ๋ผ ์ƒํƒœ๋ฅผ ๋ฐ”๊พผ๋‹ค.๋” ๊ตฌ์ฒด์ ์œผ๋กœ ๋งํ•˜๋ฉด ์†์˜ ๋™์ž‘์€ ํด๋ฆญ, ๊ฐ€๋ณ๊ฒŒ ๋‘๋“œ๋ฆฌ๊ธฐ, ๋ˆ„๋ฅด๊ธฐ ๋“ฑ์œผ๋กœ ๋ฐ”๋€๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋ชจ๋“  ์˜๋„๋Š” ์šฐ๋ฆฌ์˜ ๋‡Œ์—์„œ ์‹œ์ž‘๋œ๋‹ค.
์˜ค๋Š˜๋‚ , ์šฐ๋ฆฌ๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ์œ ํ˜•์˜ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์„ ๊ฐœ๋ฐœํ•  ๊ฒƒ์ด๋‹ค.์šฐ๋ฆฌ๋Š” ๋‹น์‹ ์˜ ์ธ์ง€ ์ƒํƒœ์— ๋”ฐ๋ผ ์ƒํƒœ๋ฅผ ๋ฐ”๊พธ๋Š” ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์„ ๊ฐœ๋ฐœํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.
๋‚ด ๋ง ๋‹ค ๋“ค์–ด.
๋งŒ์•ฝ ์šฐ๋ฆฌ์˜ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์ด ๋‹น์‹ ์˜ ํ‰์˜จ๋„์— ๋”ฐ๋ผ WebGL ํ•ด์–‘์˜ ์šด๋™์„ ๋ฐ”๊พธ๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ์š”?๋‹น์‹ ์˜ ๊ฐ๊ฐ์— ์˜ํ•ด ๊ตฌ๋™๋˜๋Š”'์‹œ๊ฐ ๋ช…์ƒ'์ฒดํ—˜.
  • View App
  • ์ฒซ ๋ฒˆ์งธ ๋‹จ๊ณ„๋Š” ์ด๋Ÿฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ธก์ •ํ•˜๊ณ  ์ ‘๊ทผํ•˜๋Š” ๊ฒƒ์ด๋‹ค.์ด๋ฅผ ์œ„ํ•ด ์šฐ๋ฆฌ๋Š” ๊ฐœ๋… ์ด์–ดํฐ์„ ์‚ฌ์šฉํ•  ๊ฒƒ์ด๋‹ค.

    ํŠธ์œ„ํ„ฐ ๊ฐœ๋… ๋„์ž…

    ์ž…๋ฌธ


    Create React ์•ฑ(CRA)์œผ๋กœ ๋จผ์ € ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค.VS ์ฝ”๋“œ์—์„œ ํ•ญ๋ชฉ์„ ์—ด๊ณ  ๋กœ์ปฌ์—์„œ ํ”„๋กœ๊ทธ๋žจ์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  • npx create-react-app mind-controlled-ocean
  • code mind-controlled-ocean
  • npm start
  • ๋งŒ์•ฝ ๋ชจ๋“  ๊ฒƒ์ด ์ˆœ์กฐ๋กญ๋‹ค๋ฉด, ๋„ˆ๋Š” ๋ฐ˜๋“œ์‹œ ์ด๋Ÿฐ ์ƒํ™ฉ์„ ๋ณด์•„์•ผ ํ•œ๋‹ค.
    React ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ ๊ธฐ๋ณธ ๋ณด๊ธฐ ๋งŒ๋“ค๊ธฐ

    ๐Ÿ”‘ ์ธ์ฆ


    ์šฐ๋ฆฌ๋Š” ํ”„๋ผ์ด๋ฒ„์‹œ๋ฅผ ๋ฏฟ๋Š”๋‹ค.์ด๊ฒƒ์ด ๋ฐ”๋กœ ๊ฐœ๋…์ด ์ฒ˜์Œ์œผ๋กœ ์‹ ๋ถ„ ๊ฒ€์ฆ ๊ธฐ๋Šฅ์„ ๊ฐ–์ถ˜ ๋‡Œ์ปดํ“จํ„ฐ์ธ ์ด์œ ๋‹ค.auth๋ฅผ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์— ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์€ ๋งค์šฐ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค.์ด๋ฅผ ์œ„ํ•ด์„œ๋Š” ์ธ์ฆ ์ƒํƒœ๋ฅผ ๋™๊ธฐํ™”ํ•˜๋Š” ๋กœ๊ทธ์ธ ํผ๊ณผ ๋ถ€์ž‘์šฉ 3๊ฐœ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
    ๋Œ€๋‡Œ ์ปดํ“จํ„ฐ์— ์—ฐ๊ฒฐํ•ด์•ผ ํ•  ๊ฒƒ์€ ๋‹จ์ง€ ํ•˜๋‚˜Neurosity account์™€ ์žฅ์น˜ ID์ž…๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ ํผ์„ ์œ„ํ•œ ์ƒˆ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์ด ๊ตฌ์„ฑ ์š”์†Œ๋Š” ์ด ์ •๋ณด๋ฅผ ์ˆ˜์ง‘ํ•ฉ๋‹ˆ๋‹ค.
    // src/components/LoginForm.js
    import React, { useState } from "react";
    
    export function LoginForm({ onLogin, loading, error }) {
      const [deviceId, setDeviceId] = useState("");
      const [email, setEmail] = useState("");
      const [password, setPassword] = useState("");
    
      function onSubmit(event) {
        event.preventDefault();
        onLogin({ deviceId, email, password });
      }
    
      return (
        <form className="card login-form" onSubmit={onSubmit}>
          <h3 className="card-heading">Login</h3>
          {!!error ? <h4 className="card-error">{error}</h4> : null}
          <div className="row">
            <label>Notion Device ID</label>
            <input
              type="text"
              value={deviceId}
              disabled={loading}
              onChange={e => setDeviceId(e.target.value)}
            />
          </div>
          <div className="row">
            <label>Email</label>
            <input
              type="email"
              value={email}
              disabled={loading}
              onChange={e => setEmail(e.target.value)}
            />
          </div>
          <div className="row">
            <label>Password</label>
            <input
              type="password"
              value={password}
              disabled={loading}
              onChange={e => setPassword(e.target.value)}
            />
          </div>
          <div className="row">
            <button type="submit" className="card-btn" disabled={loading}>
              {loading ? "Logging in..." : "Login"}
            </button>
          </div>
        </form>
      );
    }
    
    

    ๐Ÿ– Add styles the form - here's the CSS I used.


    ์ด ๊ตฌ์„ฑ ์š”์†Œ๋Š” deviceId, email ๋ฐ password ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.๋˜ํ•œ ์šฐ๋ฆฌ์˜ ํผ ๊ตฌ์„ฑ ์š”์†Œ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ ๋‹จ์ถ”๋ฅผ ๋ˆŒ๋ €์„ ๋•Œ ์ด ๋„๊ตฌ๋ฅผ ์‹คํ–‰ํ•  onLogin ๋„๊ตฌ๋ฅผ ๋ฐ›์•„๋“ค์ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค.์šฐ๋ฆฌ๋Š” ๋˜ํ•œ ํผ ์ œ์ถœ ๊ณผ์ •์—์„œ loading ์•„์ดํ…œ๊ณผ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ํ‘œ์‹œ๋˜๋Š” error ๋ฉ”์‹œ์ง€ ์•„์ดํ…œ์„ ๋ฐ›์•„๋“ค์ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค.
    ํ˜„์žฌ ๋กœ๊ทธ์ธ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ์ƒˆ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ์‚ฌ์šฉํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.
    // src/pages/Login.js
    import React, { useState, useEffect } from "react";
    import { LoginForm } from "../components/LoginForm";
    
    export function Login({ notion, user, setUser, setDeviceId }) {
      const [email, setEmail] = useState("");
      const [password, setPassword] = useState("");
      const [error, setError] = useState("");
      const [isLoggingIn, setIsLoggingIn] = useState(false);
    
      function onLogin({ email, password, deviceId }) {
        if (email && password && deviceId) {
          setError("");
          setEmail(email);
          setPassword(password);
          setDeviceId(deviceId);
        } else {
          setError("Please fill the form");
        }
      }
    
      return (
        <LoginForm
          onLogin={onLogin}
          loading={isLoggingIn}
          error={error}
        />
      );
    }
    
    ๋กœ๊ทธ์ธ ์–‘์‹ ๊ตฌ์„ฑ ์š”์†Œ
    ์ด ํŽ˜์ด์ง€์˜ ๋ชฉํ‘œ๋Š” ๋กœ๊ทธ์ธ ํผ์„ ํ‘œ์‹œํ•˜๊ณ  setError ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ๊ธฐ๋ณธ ํผ ๊ฒ€์ฆ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.ํ›„์ž์— ๋Œ€ํ•ด ๋ถ€์ž‘์šฉ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๋ถ€์ž‘์šฉ์€ email, password, ํŽ˜์ด์ง€๋ฅผ ๋ฐ›์€ ๋„๊ตฌ์™€ ๋™๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค.
    useEffect(() => {
      if (!user && notion && email && password) {
        login();
      }
    
      async function login() {
        setIsLoggingIn(true);
        const auth = await notion
          .login({ email, password })
          .catch(error => {
            setError(error.message);
          });
    
        if (auth) {
          setUser(auth.user);
        }
    
        setIsLoggingIn(false);
      }
    }, [email, password, notion, user, setUser, setError]);
    
    ๊ฐœ๋… API๋กœ ์„ค์ •๋œ ์ธ์ฆ ์‚ฌ์šฉ์ž ์„ธ์…˜์„ ์ €์žฅํ•˜๋Š” ๊ฐ์ฒด๋กœ ๊ฐ„์ฃผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.๋”ฐ๋ผ์„œ ์šฐ๋ฆฌ๋Š” ์ธ์ฆ ์„ธ์…˜์ด ์—†๋Š” ์ƒํ™ฉ์—์„œ๋งŒ user ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ์ƒํƒœ์— ๊ฐœ๋…์ ์ธ ์‹ค๋ก€๊ฐ€ ์žˆ๊ณ  ์‚ฌ์šฉ์ž๊ฐ€ ์ „์ž๋ฉ”์ผ๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ œ์ถœํ–ˆ์Šต๋‹ˆ๋‹ค.
    ๊ณง ์šฐ๋ฆฌ๊ฐ€ ์–ด๋–ป๊ฒŒ ์•„์ดํ…œ์„ ์–ป์„ ์ˆ˜ ์žˆ๋Š”์ง€ ์•Œ๊ฒŒ ๋  ๊ฒƒ์ด๋‹ค: login().๊ทธ๋Ÿฌ๋‚˜ ์šฐ๋ฆฌ๊ฐ€ ์ด ์ผ์„ ํ•˜๊ธฐ ์ „์— ์šฐ๋ฆฌ notion, user, setUser, setDeviceId ๋กœ ๋Œ์•„๊ฐ€ ๊ทธ๊ฒƒ์„ ๋ชจ๋‘ ํ•จ๊ป˜ ๋‘๊ธฐ ์‹œ์ž‘ํ•˜์ž.

    โš™๏ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ƒํƒœ


    ์ด ํ”„๋กœ๊ทธ๋žจ์˜ ๋‹จ์ˆœ์„ฑ์„ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด์„œ, ์šฐ๋ฆฌ๋Š” React์˜ App.js ๊ฐˆ๊ณ ๋ฆฌ, Reach ๊ณต์œ ๊ธฐ, react-use ๐Ÿ‘ ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‹น์‹ ์—๊ฒŒ ๊ฐ€์ ธ๋‹ค ์ค„ ๋กœ์ปฌ ์ €์žฅ ๊ฐˆ๊ณ ๋ฆฌ๋งŒ ์‚ฌ์šฉํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.์ด๊ฒƒ์€ ์šฐ๋ฆฌ์˜ ์ผ๋ฐ˜์ ์ธ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ ์ƒํƒœ ์ •์ฑ…์€ ์ „์—ญ ์ƒํƒœ๋ฅผ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ ๊ตฌ์„ฑ ์š”์†Œ ์ˆ˜์ค€์œผ๋กœ ์œ ์ง€ํ•˜๊ณ  ํ•„์š”ํ•œ ๋„๊ตฌ๋ฅผ ํ•˜์œ„ ๊ตฌ์„ฑ ์š”์†Œ์— ์ „๋‹ฌํ•˜๋Š” ๊ฒƒ์„ ํฌํ•จํ•  ๊ฒƒ์ด๋‹ค.
  • useState
  • ์šฐ๋ฆฌ๋Š” ํ•˜๋‚˜์˜ ๋…ธ์„ ์—์„œ ์‹œ์ž‘ํ•  ๊ฒƒ์ด์ง€๋งŒ, ์šฐ๋ฆฌ๊ฐ€ ๊ณ„์† ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์„ ๊ตฌ์ถ•ํ•จ์— ๋”ฐ๋ผ, ์šฐ๋ฆฌ๋Š” ๋‘ ๊ฐœ์˜ ๋…ธ์„ ์„ ์ถ”๊ฐ€ํ•  ๊ฒƒ์ด๋‹ค.
    // src/App.js
    import React, { useState, useEffect } from "react";
    import { Router, navigate } from "@reach/router";
    import useLocalStorage from "react-use/lib/useLocalStorage";
    import { Login } from "./pages/Login";
    
    export function App() {
      const [notion, setNotion] = useState(null);
      const [user, setUser] = useState(null);
      const [deviceId, setDeviceId] = useLocalStorage("deviceId");
      const [loading, setLoading] = useState(true);
    
      return (
        <Router>
          <Login
            path="/"
            notion={notion}
            user={user}
            setUser={setUser}
            setDeviceId={setDeviceId}
          />
        </Router>
      );
    }
    

    ๐Ÿ† I found the Reach Router by Ryan Florence (and friends) to be the perfect balance between simplicity and predictability. Their documentation is extremely clear and allowed me to implement basic routing within minutes.


    ๋กœ์ปฌ ์ €์žฅ์†Œ์— npm install @reach/router react-use ๋ฅผ ์ €์žฅํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ•œ ์ด์œ ๋ฅผ ์•Œ๊ณ  ์‹ถ๋‹ค๋ฉด, ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ•˜๊ธฐ ์ „๊ณผ ํ›„์— ์ ‘๊ทผํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.๊ทธ๊ฒƒ์€ ์—ฌ๋Ÿฌ ๋ฒˆ ๋“ค์–ด๊ฐˆ ํ•„์š”๊ฐ€ ์—†์„ ์ •๋„๋กœ ๋” ์ข‹์€ ์‚ฌ์šฉ์ž ์ฒดํ—˜์„ ์ œ๊ณตํ•œ๋‹ค.

    ๐Ÿง  ๊ฐœ๋…


    ์ด์ œ ์šฐ๋ฆฌ๋Š” API๋ฅผ ์„ค์น˜ํ•˜๊ณ  ๊ฐ€์ ธ์˜ค๊ธฐdeviceId๋ฅผ ํ†ตํ•ด ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ๊ณผ ๊ฐœ๋…์„ ํ†ตํ•ฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋ณธ์ ์ธ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ๊ฐ–์ถ”์—ˆ๋‹ค.
  • App.js
  • ๐Ÿคฏ The Notion API enables full communication between the device and the app. We'll use it to get real-time feedback based on the user's cognitive state. For this app, we'll work specifically with the calm API.


    import { Notion } from "@neurosity/notion";
    
    ๊ฐœ๋… ์žฅ์น˜์— ์—ฐ๊ฒฐํ•˜๋Š” ๊ฒƒ์€ ๋งค์šฐ ๊ฐ„๋‹จํ•˜๋‹ค.์šฐ๋ฆฌ๋Š” ์ƒˆ๋กœ์šด ๊ฐœ๋…์„ ์‹ค๋ก€ํ™”ํ•˜๊ณ  ์žฅ์น˜ ID๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ๋ถ€์ž‘์šฉ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. npm install @neurosity/notion ๊ณผ ๋™๊ธฐํ™”ํ•˜์—ฌ ์‹ค๋ก€๋ฅผ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ ๊ตฌ์„ฑ ์š”์†Œ ์ƒํƒœ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    ๐Ÿ“– docs.neurosity.co์—์„œ ์ „์ฒด ๊ฐœ๋… ๋ฌธ์„œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    useEffect(() => {
      if (deviceId) {
        const notion = new Notion({ deviceId }); // ๐Ÿ˜ฒ
        setNotion(notion);
      } else {
        setLoading(false);
      }
    }, [deviceId]);
    
    ์šฐ๋ฆฌ๊ฐ€ ๋™๊ธฐํ™”ํ•˜๊ณ ์ž ํ•˜๋Š” ๋˜ ๋‹ค๋ฅธ ์ƒํƒœ๋Š” deviceId ์ƒํƒœ์ด๋‹ค.
    ๋‹ค์Œ ์˜ˆ์‹œ์—์„œ ์šฐ๋ฆฌ๋Š” user ์‹ค๋ก€์˜ ๊ฐ’๊ณผ ๋™๊ธฐํ™”ํ•˜๋Š” ๋ถ€์ž‘์šฉ์„ ์ถ”๊ฐ€ํ•  ๊ฒƒ์ด๋‹ค.๋งŒ์•ฝ notion ์ด ์„ค์ •๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด, notion ์‹ค๋ก€๋ฅผ ๋งŒ๋“ค๊ธฐ ์ „์—, ์šฐ๋ฆฌ๋Š”calm ์ด๋ฒคํŠธ ๊ตฌ๋…์„ ๊ฑด๋„ˆ๋›ธ ๊ฒƒ์ž…๋‹ˆ๋‹ค.
    useEffect(() => {
      if (!notion) {
        return;
      }
    
      const subscription = notion.onAuthStateChanged().subscribe(user => {
        if (user) {
          setUser(user);
        } else {
          navigate("/");
        }
        setLoading(false);
      });
    
      return () => {
        subscription.unsubscribe();
      };
    }, [notion]);
    
    ๋งŒ์•ฝ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์— ํ™œ์„ฑ ์‚ฌ์šฉ์ž ์„ธ์…˜์ด ์žˆ๋‹ค๋ฉด, ์ด ์„ธ์…˜์€ ๊ฐœ๋… ๊ฒ€์ฆ์— ์˜ํ•ด ์ง€์†๋ฉ๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ํ˜„์žฌ ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๋ฅผ ์–ป๊ณ  ์ด๋ฅผ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ ๊ตฌ์„ฑ ์š”์†Œ์˜ ์ƒํƒœ๋กœ ์„ค์ •ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.notion ๋ฐฉ๋ฒ•์€ ๊ด€์ฐฐํ•  ์ˆ˜ ์žˆ๋Š” ์‚ฌ์šฉ์ž ์ธ์ฆ ์ด๋ฒคํŠธ๋ฅผ ๋˜๋Œ๋ ค์ค๋‹ˆ๋‹ค.์ฃผ์˜ํ•ด์•ผ ํ•  ๊ฒƒ์€ ๋ธŒ๋ผ์šฐ์ €์—์„œ ๊ฐœ๋… API๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์„ธ์…˜์€ ๋กœ์ปฌ ์ €์žฅ์†Œ๋ฅผ ํ†ตํ•ด ์ง€์†๋œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.๋”ฐ๋ผ์„œ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์„ ๋‹ซ๊ฑฐ๋‚˜ ํŽ˜์ด์ง€๋ฅผ ๋‹ค์‹œ ๋กœ๋“œํ•˜๋ฉด ์„ธ์…˜์ด ์ง€์†๋˜๊ณ  onAuthStateChanged ์ด ์•„๋‹Œ ์‚ฌ์šฉ์ž ์„ธ์…˜์œผ๋กœ ๋Œ์•„๊ฐ‘๋‹ˆ๋‹ค.์ด๊ฒƒ์ด ๋ฐ”๋กœ ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.
    ์„ธ์…˜์ด ๊ฐ์ง€๋˜์ง€ ์•Š์œผ๋ฉด ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด onAuthStateChanged ์„ ์–ด์…ˆ๋ธ”๋ฆฌ ์ƒํƒœ๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
    ์šฐ๋ฆฌ๋Š” ๋กœ๊ทธ์•„์›ƒ ํŽ˜์ด์ง€๋ฅผ ์ถ”๊ฐ€ํ•ด์„œ ์™„์ „ํ•œ ์ธ์ฆ์„ ์™„์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.
    // src/pages/Logout.js
    import { useEffect } from "react";
    import { navigate } from "@reach/router";
    
    export function Logout({ notion, resetState }) {
      useEffect(() => {
        if (notion) {
          notion.logout().then(() => {
            resetState();
            navigate("/");
          });
        }
      }, [notion, resetState]);
    
      return null;
    }
    
    ๋กœ๊ทธ์•„์›ƒ ํŽ˜์ด์ง€๋Š” React ๊ตฌ์„ฑ ์š”์†Œ์ผ ๋ฟ DOM ์š”์†Œ๋Š” ์—†์Šต๋‹ˆ๋‹ค.์šฐ๋ฆฌ๊ฐ€ ํ•„์š”๋กœ ํ•˜๋Š” ์œ ์ผํ•œ ๋…ผ๋ฆฌ๋Š” ๋ถ€์ž‘์šฉ์ด๋‹ค. ๋งŒ์•ฝ null ์‹ค๋ก€๊ฐ€ ์กด์žฌํ•œ๋‹ค๋ฉด, ๊ทธ๊ฒƒ์€ user ๋ฐฉ๋ฒ•์„ ํ˜ธ์ถœํ•  ๊ฒƒ์ด๋‹ค.๋งˆ์ง€๋ง‰์œผ๋กœ, ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์•„์›ƒํ•œ ํ›„์— ์ดˆ๊ธฐ ๊ฒฝ๋กœ๋กœ ๋‹ค์‹œ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
    ์ด ๊ตฌ์„ฑ ์š”์†Œ๋Š” ํ˜„์žฌ notion.logout() ์—์„œ ๋ผ์šฐํŒ…์œผ๋กœ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    // src/App.js
    // ...
    import { Logout } from "./pages/Logout";
    // ...
    
    return (
      <Router>
        {/* ... */}
        <Logout path="/logout" notion={notion} resetState={() => {
          setNotion(null);
          setUser(null);
          setDeviceId("");
        }} />
      </Router>
    );
    
    ์ด์ œ ๊ฒ€์ฆ์ด ๋๋‚ฌ์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ์˜ ์ธ์ง€ ์ƒํƒœ์— ๋”ฐ๋ผ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ ๋…ผ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•ฉ์‹œ๋‹ค!

    ๐ŸŒŠ WebGL ํ•ด์–‘


    David์˜ WebGL ocean์„ ๋ณด์•˜์„ ๋•Œ, ๋‚˜๋Š” ๊ทธ๊ฒƒ์„ ์‚ฌ๋ž‘ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.๊ทธ๋ž˜์„œ ์ด ๊ฐœ๋…์œผ๋กœ ๋‚ ์”จ๊ฐ€ ํŒŒ๋„๋ฅผ ์›€์ง์ด๋Š” ๋ฐ ์˜ํ–ฅ์„ ์ฃผ๋Š” ๊ฒƒ์€ ํฅ๋ฏธ๋กœ์šด ์‹คํ—˜๊ณผ ๊ฐ™๋‹ค.

    ๐Ÿ’ก Fun fact: This ocean wave simulation was created in 2013 by David Li with JavaScript and WebGL. It has been featured as part of Google Experiments. I was happy to see it was open-sourced under the MIT license.


    ๋‹ค์Œ ๋ถ€๋ถ„์—์„œ ์šฐ๋ฆฌ์˜ ์ƒ๊ฐ์€ ์ƒˆ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด ๊ตฌ์„ฑ ์š”์†Œ๋Š” WebGL ocean์„ ์‚ฌ์šฉํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.๋”ฐ๋ผ์„œ Ocean notion ์ด๋ผ๋Š” ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ๋งŒ๋“ค๊ณ  ๋‹ค์Œ ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  • simulation.js
  • weather.js
  • ํ•ด์–‘.js:
  • // src/components/Ocean/Ocean.js
    import React, { useState, useEffect, useRef } from "react";
    import useRafState from "react-use/lib/useRafState";
    
    import { Simulator, Camera } from "./simulation.js"; // by David Li
    import { mapCalmToWeather } from "./weather.js";
    
    const camera = new Camera();
    
    export function Ocean({ calm }) {
      const ref = useRef();
      const [simulator, setSimulator] = useState();
      const [lastTime, setLastTime] = useRafState(Date.now());
    
      useEffect(() => {
        const { innerWidth, innerHeight } = window;
        const simulator = new Simulator(ref.current, innerWidth, innerHeight);
        setSimulator(simulator);
      }, [ref, setSimulator]);
    
      useEffect(() => {
        if (simulator) {
          const currentTime = Date.now();
          const deltaTime = (currentTime - lastTime) / 1000 || 0.0;
          setLastTime(currentTime);
          simulator.render(deltaTime, camera);
        }
      }, [simulator, lastTime, setLastTime]);
    
      return <canvas className="simulation" ref={ref}></canvas>;
    }
    
    ๋งŒ์•ฝ ๋ชจ๋“  ๊ฒƒ์ด ์ˆœ์กฐ๋กญ๋‹ค๋ฉด, ์šฐ๋ฆฌ๋Š” ๋ฐ˜๋“œ์‹œ ์ด ์ ์„ ๋ณด์•„์•ผ ํ•œ๋‹ค.
    ํŒŒ๋„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
    ์—ฌ๊ธฐ์„œ ์ผ์–ด๋‚œ ์ผ์„ ์ž์„ธํžˆ ์†Œ๊ฐœํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
  • 1๏ธโƒฃ React ๊ตฌ์„ฑ ์š”์†Œ๋Š” WebGL 3D ์žฅ๋ฉด์˜ ์บ”๋ฒ„์Šค ์š”์†Œ
  • ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • 2๏ธโƒฃ ReactApp.js๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ canvas HTML ์š”์†Œ
  • ์— ์•ก์„ธ์Šคํ•ฉ๋‹ˆ๋‹ค.
  • 3๏ธโƒฃ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ธ์šฉํ•  ๋•Œ, ์šฐ๋ฆฌ๋Š” ์ƒˆ๋กœ์šด ./src/components/Ocean ์„ ์‹ค๋ก€ํ™”ํ•ฉ๋‹ˆ๋‹ค.useRef๋ฅ˜๋Š” ๋ฐ”๋žŒ, ๊ธฐ๋ณต๋„, ํฌ๊ธฐ ๋“ฑ ๋ Œ๋”๋ง๊ณผ ๋‚ ์”จ ์†์„ฑ์„ ์ œ์–ดํ•œ๋‹ค.
  • 4๏ธโƒฃ ์šฐ๋ฆฌ๋Š” Simulator (request Animation Frame) ๊ฐˆ๊ณ ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ชจ๋“  ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ”„๋ ˆ์ž„์—์„œ ์‹คํ–‰๋˜๋Š” ์ˆœํ™˜์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
  • ์ด๋•Œ ์šฐ๋ฆฌ์˜ ํŒŒ๋„๋Š” ์ •์  ๋‚ ์”จ์น˜์— ๋”ฐ๋ผ ๊ธฐ๋ณต๋„, ๋ฐ”๋žŒ๊ณผ ํฌ๊ธฐ๋กœ ์ด๋™ํ•œ๋‹ค.๊ทธ๋ ‡๋‹ค๋ฉด ์šฐ๋ฆฌ๋Š” ์–ด๋–ป๊ฒŒ Simulator๋ถ„์ˆ˜์— ๋”ฐ๋ผ ์ด๋Ÿฐ ๋‚ ์”จ ์„ค์ •์„ ๋น„์ถ”๋‚˜์š”?
    ์ด๋ฅผ ์œ„ํ•ด ์ €๋Š” useRaf์—์„œ ํ‰์˜จํ•œ ์ ์ˆ˜๋ฅผ ์ƒ์‘ํ•˜๋Š” ๋‚ ์”จ ์„ค์ •์ธ ๊ธฐ๋ณต๋„, ๋ฐ”๋žŒ๊ณผ ํฌ๊ธฐ์— ๋น„์ถ”๋Š” ์‹ค์šฉ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.๊ทธ๋ฆฌ๊ณ  ์šฐ๋ฆฌ๋Š” ๋งค๋ฒˆ calm ์ ์ˆ˜๊ฐ€ ๋ณ€ํ•  ๋•Œ๋งˆ๋‹ค ๋™๊ธฐํ™”ํ•˜๋Š” ๋ถ€์ž‘์šฉ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.
    useEffect(() => {
      if (simulator) {
        setWeatherBasedOnCalm(animatedCalm, 0, 0);
      }
    
      function setWeatherBasedOnCalm(calm) {
        const { choppiness, wind, size } = mapCalmToWeather(calm);
        simulator.setChoppiness(choppiness);
        simulator.setWind(wind, wind);
        simulator.setSize(size);
      }
    }, [calm, simulator]);
    

    ์ธ์ง€ ์ƒํƒœ


    ์ด๊ฒƒ์€ ์žฌ๋ฏธ์žˆ๋Š” ๋ถ€๋ถ„์ด๋‹ค.์ด๊ฒƒ์€ ์šฐ๋ฆฌ๊ฐ€ ๋Œ€๋‡Œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐฉ๋ฌธํ•˜์—ฌ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ ์ƒํƒœ์— ๋น„์ถ”๋Š” ๊ณณ์ด๋‹ค.
    ๊ตฌ๋…weather.js์„ ํ†ตํ•ด ์šฐ๋ฆฌ๋Š” ์•ฝ 1์ดˆ์— ์ƒˆ๋กœ์šด calm ์ ์ˆ˜๋ฅผ ์–ป๋Š”๋‹ค.๊ทธ๋Ÿฌ๋ฉด notion.calm() ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  calm ๋„๊ตฌ๋กœ ์ถ”๊ฐ€ํ•˜๋ฉฐ <Ocean calm={calm} /> ๋ฐ calm ์‹ค๋ก€์™€ ๋™๊ธฐํ™”๋œ ๋ถ€์ž‘์šฉ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.๋งŒ์•ฝ ์ด ๋‘ ๊ฐ€์ง€ ์ƒํƒœ๊ฐ€ ๋ชจ๋‘ ์กด์žฌํ•œ๋‹ค๋ฉด, ์šฐ๋ฆฌ๋Š” ์•ˆ์‹ฌํ•˜๊ณ calm๋ฅผ ๋ฐ›์•„๋“ค์ผ ์ˆ˜ ์žˆ๋‹ค.

    ๐Ÿง˜๐Ÿฟโ€โ™€๏ธ The calm score is derived from your passive cognitive state. This metric based on the alpha wave. The calm score ranges from 0.0 to 1.0. The higher the score, the higher the probability a calm feeling is detected. Getting a calm score over 0.3 is significant. Things that can help increase the calm score include closing your eyes, keeping still, breathing deeply, or meditating.


    // src/pages/Calm.js
    import React, { useState, useEffect } from "react";
    import { Ocean } from "../components/Ocean/Ocean";
    
    export function Calm({ user, notion }) {
      const [calm, setCalm] = useState(0);
    
      useEffect(() => {
        if (!user || !notion) {
          return;
        }
    
        const subscription = notion.calm().subscribe(calm => {
          const calmScore = Number(calm.probability.toFixed(2));
          setCalm(calmScore);
        });
    
        return () => {
          subscription.unsubscribe();
        };
      }, [user, notion]);
    
      return (
        <Ocean calm={calm} />
      );
    }
    

    ๐Ÿ’ก All notion metrics, including notion.calm() return an RxJS subscription that we can use to safely unsubscribe when the component unmounts.


    ๋งˆ์ง€๋ง‰์œผ๋กœ ์ž”์ž”ํ•œ ํŽ˜์ด์ง€๋ฅผ notion์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
    // src/App.js
    // ...
    import { Calm } from "./pages/Calm";
    // ...
    
    // If already authenticated, redirect user to the Calm page
    useEffect(() => {
      if (user) {
        navigate("/calm");
      }
    }, [user]);
    
    return (
      <Router>
        {/* ... */}
        <Calm path="/calm" notion={notion} user={user} />
      </Router>
    );
    
    ์ด๋กœ์จ Neuro React ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • View full code
  • ์‹ ๊ฒฝ์ฆ / ๊ฐœ๋… ํ•ด์–‘


    ๐ŸŒŠ ๋‡Œ ์ปดํ“จํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ WebGL ํ•ด์–‘์˜ ์šด๋™์„ ์ œ์–ดํ•˜๋‹ค


    ๋‚˜๋Š” ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ ์ฒดํ—˜์— ๋Œ€ํ•ด ํฅ๋ถ„์„ ๋Š๊ผˆ๋‹ค. ์ด๋Ÿฐ ์ฒดํ—˜๋“ค์€ ์šฐ๋ฆฌ๊ฐ€ ํ•œ ์‚ฌ๋žŒ์œผ๋กœ์„œ์˜ ์˜ํ–ฅ์„ ๋ฐ›์•˜๋‹ค.๋ชจ๋“  ๋‡Œ๋Š” ๋‹ค๋ฅด์ง€๋งŒ, ์šฐ๋ฆฌ๋Š” ๋Š์ž„์—†์ด ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์„ ๊ฐœ๋ฐœํ•˜์—ฌ ๋ชจ๋“  ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ฐ™์€ ์ฒดํ—˜์„ ์ œ๊ณตํ•œ๋‹ค.๋งŒ์•ฝ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์ด ๋‹น์‹ ์„ ์œ„ํ•ด ๋งž์ถคํ˜• ์ œ์ž‘์„ ํ•œ๋‹ค๋ฉด?
    ๋งŒ์•ฝ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์ด ๋‹น์‹ ์ด ์ŠคํŠธ๋ ˆ์Šค ์†์—์„œ ๊ธด์žฅ์„ ํ‘ธ๋Š” ๊ฒƒ์„ ๋„์šธ ์ˆ˜ ์žˆ๋‹ค๋ฉด?
    ๋งŒ์•ฝ ๋‹น์‹ ์˜ ๋‡ŒํŒŒ๋กœ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด?
    ๋งŒ์•ฝ ๋น„๋””์˜ค ๊ฒŒ์ž„์ด ๋‹น์‹ ์˜ ๋Š๋‚Œ์— ๋”ฐ๋ผ ๊ทธ๋“ค์˜ ์„œ์ˆ ์„ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋‹ค๋ฉด?
    ๋งŒ์•ฝ... ์–ด๋–กํ•˜์ง€...

    ์ข‹์€ ์›นํŽ˜์ด์ง€ ์ฆ๊ฒจ์ฐพ๊ธฐ