본문 바로가기

좋아하는 것_매직IT/96.IT 핫이슈

Ruck - Deno용 Buildless React 웹 프레임워크 (ruck.tech)

반응형

Ruck - Deno용 Buildless React 웹 프레임워크에 대해서 소개합니다.

Ruck 에 대해서 홈페이지는 아래와 같이 설명하고 있군요..
An open source buildless React web application framework for Deno.

한마디로, Ruck 은 Deno용 Buildless React 웹 프레임워크라고 머리속에 넣어두시면 좋을것 같습니다.

아래는 깃허브 페이지고요..

Ruck은 Deno용 오픈 소스 빌드리스 React 웹 애플리케이션 프레임워크이며, 기본 사이트나 강력한 앱을 만드는 데 사용할 수 있다고 하고요..
ESM, 동적 가져오기, HTTP 가져오기 및 가져오기 맵과 같은 최첨단 표준 기술을 사용하여 변환 또는 번들링과 같은 빌드 단계를 방지한다고 하네요..

설치는 아래와 같이 진행하시면 된다고 하고요..

Installation

A Ruck project contains:

  • A Deno config file called deno.json or deno.jsonc, containing:This enables Deno and DOM types for project and imported dependency modules.
  • {
      "compilerOptions": {
        "lib": [
          "dom",
          "dom.iterable",
          "dom.asynciterable",
          "deno.ns",
          "deno.unstable"
        ]
      }
    }
  • Import map JSON files that tell your IDE, Deno, and browsers where to import dependencies from. Ruck automatically uses es-module-shims so you don’t need to worry about poor browser support for import maps.Recommended import map file names and starter contents:
    • importMap.server.dev.json
    • {
        "imports": {
          "graphql-react/": "https://unpkg.com/graphql-react@19.0.0/",
          "react": "https://esm.sh/react@17.0.2?target=deno&dev",
          "react-dom/server": "https://esm.sh/react-dom@17.0.2/server?target=deno&dev",
          "react-waterfall-render/": "https://unpkg.com/react-waterfall-render@4.0.1/",
          "ruck/": "https://deno.land/x/ruck@v7.0.0/",
          "std/": "https://deno.land/std@0.151.0/"
        }
      }
    • importMap.server.json
    • {
        "imports": {
          "graphql-react/": "https://unpkg.com/graphql-react@19.0.0/",
          "react": "https://esm.sh/react@17.0.2?target=deno",
          "react-dom/server": "https://esm.sh/react-dom@17.0.2/server?target=deno",
          "react-waterfall-render/": "https://unpkg.com/react-waterfall-render@4.0.1/",
          "ruck/": "https://deno.land/x/ruck@v7.0.0/",
          "std/": "https://deno.land/std@0.151.0/"
        }
      }
    • importMap.client.dev.jsonThe esm.sh URLs should be regularly updated to the latest build URLs these modules re-export:
    • {
        "imports": {
          "graphql-react/": "https://unpkg.com/graphql-react@19.0.0/",
          "react": "https://esm.sh/v90/react@17.0.2/es2021/react.development.js",
          "react-dom": "https://esm.sh/v90/react-dom@17.0.2/es2021/react-dom.development.js",
          "react-waterfall-render/": "https://unpkg.com/react-waterfall-render@4.0.1/",
          "ruck/": "https://deno.land/x/ruck@v7.0.0/"
        }
      }
    • importMap.client.jsonThe esm.sh URLs should be regularly updated to the latest build URLs these modules re-export:
    • {
        "imports": {
          "graphql-react/": "https://unpkg.com/graphql-react@19.0.0/",
          "react": "https://esm.sh/v90/react@17.0.2/es2021/react.js",
          "react-dom": "https://esm.sh/v90/react-dom@17.0.2/es2021/react-dom.js",
          "react-waterfall-render/": "https://unpkg.com/react-waterfall-render@4.0.1/",
          "ruck/": "https://deno.land/x/ruck@v7.0.0/"
        }
      }
    A DRY approach is to Git ignore the import map files and generate them with a script that’s a single source of truth.
  • Ideally use separate development and production import maps for the server and client. This way a version of React that has more detailed error messages can be used during local development, and server specific dependencies can be excluded from the browser import map for a faster page load.
  • A module that imports and uses Ruck’s serve function to start the Ruck app server, typically called scripts/serve.mjs. Here’s an example:The Deno CLI is used to run this script; Ruck doesn’t have a CLI.
    #!/bin/sh
    # Serves the Ruck app.
    
    # Asserts an environment variable is set.
    # Argument 1: Name.
    # Argument 2: Value.
    assertEnvVar() {
      if [ -z "$2" ]
      then
        echo "Missing environment variable \`$1\`." >&2
        exit 1
      fi
    }
    
    # Assert environment variables are set.
    assertEnvVar RUCK_DEV $RUCK_DEV
    assertEnvVar RUCK_PORT $RUCK_PORT
    
    # Serve the Ruck app.
    if [ "$RUCK_DEV" = "true" ]
    then
      deno run \
        --allow-env \
        --allow-net \
        --allow-read \
        --import-map=importMap.server.dev.json \
        --watch=. \
        scripts/serve.mjs
    else
      deno run \
        --allow-env \
        --allow-net \
        --allow-read \
        --import-map=importMap.server.json \
        --no-check \
        scripts/serve.mjs
    fi
    First, ensure it’s executable:Then, run it like this:You may choose to store environment variables in a Git ignored scripts/.env.sh file:Then, you could create a scripts/dev.sh shell script (also ensure it’s executable):This way you only need to run this when developing your Ruck app:There isn’t a universally “correct” way to use environment variables or start serving the Ruck app; create an optimal workflow for your particular development and production environments.
  • ./scripts/dev.sh
  • #!/bin/sh
    # Loads the environment variables and serves the Ruck app.
    
    # Load the environment variables.
    . scripts/.env.sh &&
    
    # Serve the Ruck app.
    ./scripts/serve.sh
  • export RUCK_DEV="true"
    export RUCK_PORT="3000"
  • RUCK_DEV="true" RUCK_PORT="3000" ./scripts/serve.sh
  • chmod +x ./scripts/serve.sh
  • You may choose to create a scripts/serve.sh shell script that serves the Ruck app:
  • // @ts-check
    
    import serve from "ruck/serve.mjs";
    
    serve({
      clientImportMap: new URL(
        Deno.env.get("RUCK_DEV") === "true"
          ? "../importMap.client.dev.json"
          : "../importMap.client.json",
        import.meta.url,
      ),
      port: Number(Deno.env.get("RUCK_PORT")),
    });
    
    console.info(
      `Ruck app HTTP server listening on http://localhost:${
        Deno.env.get("RUCK_PORT")
      }`,
    );
  • A public directory containing files that Ruck serves directly to browsers, by default called public. For example, public/favicon.ico could be accessed in a browser at the URL path /favicon.ico.
  • A router.mjs module in the public directory that default exports a function that Ruck calls on both the server and client with details such as the route URL to determine what the route content should be. It should have this JSDoc type:Ruck provides an (optional) declarative system for automatic loading and unloading of component CSS file dependencies served by Ruck via the public directory or CDN. Ruck’s routePlanForContentWithCss function can be imported and used to create route plan for content with CSS file dependencies.
    // @ts-check
    
    import { createElement as h } from "react";
    import routePlanForContentWithCss from "ruck/routePlanForContentWithCss.mjs";
    
    // The component used to display a route loading error (e.g. due to an
    // internet dropout) should be imported up front instead of dynamically
    // importing it when needed, as it would likely also fail to load.
    import PageError, {
      // A `Set` instance containing CSS URLs.
      css as cssPageError,
    } from "./components/PageError.mjs";
    
    /**
     * Gets the Ruck app route plan for a URL.
     * @type {import("ruck/serve.mjs").Router}
     */
    export default function router(url, headManager, isInitialRoute) {
      if (url.pathname === "/") {
        return routePlanForContentWithCss(
          // Dynamically import route components so they only load when needed.
          import("./components/PageHome.mjs").then(
            ({ default: PageHome, css }) => ({
              content: h(PageHome),
              css,
            }),
            // It’s important to handle dynamic import loading errors.
            catchImportContentWithCss,
          ),
          headManager,
          isInitialRoute,
        );
      }
    
      if (url.pathname === "/blog") {
        return routePlanForContentWithCss(
          import("./components/PageBlog.mjs").then(
            ({ default: PageBlog, css }) => ({
              content: h(PageBlog),
              css,
            }),
            catchImportContentWithCss,
          ),
          headManager,
          isInitialRoute,
        );
      }
    
      // For routes with URL slugs, use RegEx that only matches valid slugs,
      // instead of simply extracting the whole slug. This way an invalid URL slug
      // naturally results in an immediate 404 error and avoids loading the route
      // component or loading data with the invalid slug.
      const matchPagePost = url.pathname.match(/^\/blog\/(?<postId>[\w-]+)$/u);
    
      if (matchPagePost?.groups) {
        const { postId } = matchPagePost.groups;
    
        return routePlanForContentWithCss(
          import("./components/PagePost.mjs").then(
            ({ default: PagePost, css }) => ({
              content: h(PagePost, { postId }),
              css,
            }),
          ),
          headManager,
          isInitialRoute,
        );
      }
    
      // Fallback to a 404 error page.
      return routePlanForContentWithCss(
        // If you have a component specifically for a 404 error page, it would be
        // ok to dynamically import it here. In this particular example the
        // component was already imported for the loading error page.
        {
          content: h(PageError, {
            status: 404,
            title: "Error 404",
            description: "Something is missing.",
          }),
          css: cssPageError,
        },
        headManager,
        isInitialRoute,
      );
    }
    
    /**
     * Catches a dynamic import error for route content with CSS.
     * @param {Error} cause Import error.
     * @returns {import("ruck/routePlanForContentWithCss.mjs").RouteContentWithCss}
     */
    function catchImportContentWithCss(cause) {
      console.error(new Error("Import rejection for route with CSS.", { cause }));
    
      return {
        content: h(PageError, {
          status: 500,
          title: "Error loading",
          description: "Unable to load.",
        }),
        css: cssPageError,
      };
    }
    For the previous example, here’s the public/components/PageError.mjs module:
  • // @ts-check
    
    import { createElement as h, useContext } from "react";
    import TransferContext from "ruck/TransferContext.mjs";
    
    import Heading, { css as cssHeading } from "./Heading.mjs";
    import Para, { css as cssPara } from "./Para.mjs";
    
    // Export CSS URLs for the component and its dependencies.
    export const css = new Set([
      ...cssHeading,
      ...cssPara,
      "/components/PageError.css",
    ]);
    
    /**
     * React component for an error page.
     * @param {object} props Props.
     * @param {number} props.status HTTP status code.
     * @param {number} props.title Error title.
     * @param {string} props.description Error description.
     */
    export default function PageError({ status, title, description }) {
      // Ruck’s transfer (request/response) context; only populated on the server.
      const ruckTransfer = useContext(TransferContext);
    
      // If server side rendering, modify the HTTP status code for the Ruck app
      // page response.
      if (ruckTransfer) ruckTransfer.responseInit.status = status;
    
      return h(
        "section",
        { className: "PageError__section" },
        h(Heading, null, title),
        h(Para, null, description),
      );
    }
  • Here is an example for a website that has a home page, a /blog page that lists blog posts, and a /blog/post-id-slug-here page for individual blog posts:
  • /** @type {import("ruck/serve.mjs").Router} */
  • A components/App.mjs module in the public directory that default exports a React component that renders the entire app. It should have this JSDoc type:It typically imports and uses several React hooks from Ruck:
    • useCss to declare CSS files that apply to the entire app.
    • useHead to establish head tags that apply to the entire app such as meta[name="viewport"] and link[rel="manifest"].
    • useRoute to get the current route URL and content, and render it in a persistent layout containing global content such as a header and footer.
    Here’s an example public/components/App.mjs module for a website with home and blog pages:Ruck app route navigation links make use of these React hooks from Ruck:
    • useRoute to get the current route URL path for comparison with the link’s URL path to determine active state.
    • useOnClickRouteLink to replace the default browser navigation that happens when a link is clicked with a Ruck client side route navigation.
    For the previous example, here’s the public/components/NavLink.mjs module:
  • // @ts-check
    
    import { createElement as h } from "react";
    import useOnClickRouteLink from "ruck/useOnClickRouteLink.mjs";
    import useRoute from "ruck/useRoute.mjs";
    
    export const css = new Set([
      "/components/NavLink.css",
    ]);
    
    /**
     * React component for a navigation link.
     * @param {object} props Props.
     * @param {string} props.href Link URL.
     * @param {import("react").ReactNode} [props.children] Children.
     */
    export default function NavLink({ href, children }) {
      const route = useRoute();
      const onClick = useOnClickRouteLink();
    
      let className = "NavLink__a";
      if (href === route.url.pathname) className += " NavLink__a--active";
    
      return h("a", { className, href, onClick }, children);
    }
  • // @ts-check
    
    import { createElement as h, Fragment, useMemo } from "react";
    import useCss from "ruck/useCss.mjs";
    import useHead from "ruck/useHead.mjs";
    import useRoute from "ruck/useRoute.mjs";
    
    import NavLink, { css as cssNavLink } from "./NavLink.mjs";
    
    const css = new Set([
      ...cssNavLink,
      "/components/App.css",
    ]);
    
    /**
     * React component for the Ruck app.
     * @type {import("ruck/serve.mjs").AppComponent}
     */
    export default function App() {
      const route = useRoute();
    
      useHead(
        // Head tag fragments render in the document head in key order. A good
        // convention is to use group and subgroup numbers, followed by a
        // descriptive name.
        "1-1-meta",
        // Must be memoized. If it’s dynamic use the `useMemo` React hook,
        // otherwise define it outside the component function scope.
        useMemo(() =>
          h(
            Fragment,
            null,
            h("meta", {
              name: "viewport",
              content: "width=device-width, initial-scale=1",
            }),
            h("meta", {
              name: "og:image",
              content:
                // Sometimes an absolute URL is necessary.
                `${route.url.origin}/social-preview.png`,
            }),
            h("link", { rel: "manifest", href: "/manifest.webmanifest" }),
            // More head tags here…
          ), [route.url.origin]),
      );
    
      // This loop doesn’t break React hook rules as the list never changes.
      for (const href of css) useCss(href);
    
      return h(
        Fragment,
        null,
        // Global nav…
        h(
          "nav",
          { className: "App__nav" },
          h(NavLink, { href: "/" }, "Home"),
          h(NavLink, { href: "/blog" }, "Blog"),
        ),
        // Route content…
        route.content,
        // Global footer…
        h("footer", { className: "App__footer" }, "Global footer content."),
      );
    }
  • /** @type {import("ruck/serve.mjs").AppComponent} */

요구사항은 아래와 같고요..

Requirements



Ruck에 대해서 특징을 간단하게 정리하자면 아래와 같습니다. 

  • ESM, Dynamic Imports, HTTP Imports, Import Map등을 활용해서 트랜스파일/빌딩 단계가 없음
  • Deno 와 Browser가 코드를 직접 실행
  • 기존 프레임워크에서 복잡하거나 불가능했던 일들이 쉽게 가능
    • RegEx 나 커스텀 로직을 통한 다이나믹 라우팅
    • 컴포넌트가 TransfterContext 활용
    • useHead React hook으로 적절한 헤드 태그 렌더링
    • 컴포넌트 레벨 데이터 페칭 가능한 SSR
    • React Hook을 이용한 GraphQL 사용

그외에 자세한 내용이 궁금하신분께서는 아래 홈페이지를 방문해보시길 추천드립니다.



오늘의 블로그는 여기까지고요..
항상 믿고 봐주셔서 감사합니다.

728x90
300x250