본문 바로가기
REFLEX 튜토리얼/reflex 공식문서 요약

reflex 웹페이지는 기본적으로 클래스 하나, 함수 하나로 구성된다.

by 일코 2023. 10. 3.

reflex는 프론트엔드와 백엔드의 구분이 다소 모호(?)한 pure-python fullstack webframework이다.

reflex로 만드는 웹앱은 기본적으로 State라는 클래스 하나, 그리고 index라는 함수 하나로 구성된다.

이를 이해하기 위해서는 가장 간단한 앱 하나를 만들어보는 것이 좋다.

우선 State 없이 간단한 페이지함수인 index를 만들어보자.

import reflex as rx


def index():
    return rx.heading("Hello world!")


app = rx.App(state=State)
app.add_page(index)
app.compile()

python 코드로 구성된 간단한 소스이지만,

이는 리플렉스를 구성하고 있는

node.js, React, Next.js, Chakra-ui, FastAPI 등 다양한 프론트/백엔드 프레임워크를 통해

구체적인 html과 자바스크립트 코드로 컴파일되어 배포된다.
(화면은 아래와 같다)

(.web 폴더를 통해 컴파일 결과를 확인할 수 있지만, 추천하지는 않는다.)

위 소스코드는 어떻게 컴파일되는지 궁금하신 분들을 위해.. 아래 첨부해놓는다.

더보기
<!DOCTYPE html>
<html>
<head>
    <style data-next-hide-fouc="true">body {
        display: none
    }</style>
    <noscript data-next-hide-fouc="true">
        <style>body {
            display: block
        }</style>
    </noscript>
    <meta charSet="utf-8"/>
    <meta name="viewport" content="width=device-width"/>
    <title>Reflex App</title>
    <meta content="A Reflex app." name="description"/>
    <meta content="favicon.ico" property="og:image"/>
    <meta name="next-head-count" content="5"/>
    <noscript data-n-css=""></noscript>
    <script defer="" nomodule="" src="/_next/static/chunks/polyfills.js?ts=1696314172501"></script>
    <script src="/_next/static/chunks/webpack.js?ts=1696314172501" defer=""></script>
    <script src="/_next/static/chunks/main.js?ts=1696314172501" defer=""></script>
    <script src="/_next/static/chunks/pages/_app.js?ts=1696314172501" defer=""></script>
    <script src="/_next/static/chunks/pages/index.js?ts=1696314172501" defer=""></script>
    <script src="/_next/static/development/_buildManifest.js?ts=1696314172501" defer=""></script>
    <script src="/_next/static/development/_ssgManifest.js?ts=1696314172501" defer=""></script>
    <noscript id="__next_css__DO_NOT_USE__"></noscript>
</head>
<body>
<script id="chakra-script">!(function () {
    try {
        var a = function (c) {
            var v = "(prefers-color-scheme: dark)", h = window.matchMedia(v).matches ? "dark" : "light",
                r = c === "system" ? h : c, o = document.documentElement, s = document.body, l = "chakra-ui-light",
                d = "chakra-ui-dark", i = r === "dark";
            return s.classList.add(i ? d : l), s.classList.remove(i ? l : d), o.style.colorScheme = r, o.dataset.theme = r, r
        }, n = a, m = "light", e = "chakra-ui-color-mode", t = localStorage.getItem(e);
        t ? a(t) : localStorage.setItem(e, a(m))
    } catch (a) {
    }
})();</script>
<div id="__next">
    <style data-emotion="css-global 1b7scut">:host, :root, [data-theme] {
        --chakra-ring-inset: var(--chakra-empty, /*!*/ /*!*/);
        --chakra-ring-offset-width: 0px;
        --chakra-ring-offset-color: #fff;
        --chakra-ring-color: rgba(66, 153, 225, 0.6);
        --chakra-ring-offset-shadow: 0 0 #0000;
        --chakra-ring-shadow: 0 0 #0000;
        --chakra-space-x-reverse: 0;
        --chakra-space-y-reverse: 0;
        --chakra-colors-transparent: transparent;
        --chakra-colors-current: currentColor;
        --chakra-colors-black: #000000;
        --chakra-colors-white: #FFFFFF;
        --chakra-colors-whiteAlpha-50: rgba(255, 255, 255, 0.04);
        --chakra-colors-whiteAlpha-100: rgba(255, 255, 255, 0.06);
        --chakra-colors-whiteAlpha-200: rgba(255, 255, 255, 0.08);
        --chakra-colors-whiteAlpha-300: rgba(255, 255, 255, 0.16);
        --chakra-colors-whiteAlpha-400: rgba(255, 255, 255, 0.24);
        --chakra-colors-whiteAlpha-500: rgba(255, 255, 255, 0.36);
        --chakra-colors-whiteAlpha-600: rgba(255, 255, 255, 0.48);
        --chakra-colors-whiteAlpha-700: rgba(255, 255, 255, 0.64);
        --chakra-colors-whiteAlpha-800: rgba(255, 255, 255, 0.80);
        --chakra-colors-whiteAlpha-900: rgba(255, 255, 255, 0.92);
        --chakra-colors-blackAlpha-50: rgba(0, 0, 0, 0.04);
        --chakra-colors-blackAlpha-100: rgba(0, 0, 0, 0.06);
        --chakra-colors-blackAlpha-200: rgba(0, 0, 0, 0.08);
        --chakra-colors-blackAlpha-300: rgba(0, 0, 0, 0.16);
        --chakra-colors-blackAlpha-400: rgba(0, 0, 0, 0.24);
        --chakra-colors-blackAlpha-500: rgba(0, 0, 0, 0.36);
        --chakra-colors-blackAlpha-600: rgba(0, 0, 0, 0.48);
        --chakra-colors-blackAlpha-700: rgba(0, 0, 0, 0.64);
        --chakra-colors-blackAlpha-800: rgba(0, 0, 0, 0.80);
        --chakra-colors-blackAlpha-900: rgba(0, 0, 0, 0.92);
        --chakra-colors-gray-50: #F7FAFC;
        --chakra-colors-gray-100: #EDF2F7;
        --chakra-colors-gray-200: #E2E8F0;
        --chakra-colors-gray-300: #CBD5E0;
        --chakra-colors-gray-400: #A0AEC0;
        --chakra-colors-gray-500: #718096;
        --chakra-colors-gray-600: #4A5568;
        --chakra-colors-gray-700: #2D3748;
        --chakra-colors-gray-800: #1A202C;
        --chakra-colors-gray-900: #171923;
        --chakra-colors-red-50: #FFF5F5;
        --chakra-colors-red-100: #FED7D7;
        --chakra-colors-red-200: #FEB2B2;
        --chakra-colors-red-300: #FC8181;
        --chakra-colors-red-400: #F56565;
        --chakra-colors-red-500: #E53E3E;
        --chakra-colors-red-600: #C53030;
        --chakra-colors-red-700: #9B2C2C;
        --chakra-colors-red-800: #822727;
        --chakra-colors-red-900: #63171B;
        --chakra-colors-orange-50: #FFFAF0;
        --chakra-colors-orange-100: #FEEBC8;
        --chakra-colors-orange-200: #FBD38D;
        --chakra-colors-orange-300: #F6AD55;
        --chakra-colors-orange-400: #ED8936;
        --chakra-colors-orange-500: #DD6B20;
        --chakra-colors-orange-600: #C05621;
        --chakra-colors-orange-700: #9C4221;
        --chakra-colors-orange-800: #7B341E;
        --chakra-colors-orange-900: #652B19;
        --chakra-colors-yellow-50: #FFFFF0;
        --chakra-colors-yellow-100: #FEFCBF;
        --chakra-colors-yellow-200: #FAF089;
        --chakra-colors-yellow-300: #F6E05E;
        --chakra-colors-yellow-400: #ECC94B;
        --chakra-colors-yellow-500: #D69E2E;
        --chakra-colors-yellow-600: #B7791F;
        --chakra-colors-yellow-700: #975A16;
        --chakra-colors-yellow-800: #744210;
        --chakra-colors-yellow-900: #5F370E;
        --chakra-colors-green-50: #F0FFF4;
        --chakra-colors-green-100: #C6F6D5;
        --chakra-colors-green-200: #9AE6B4;
        --chakra-colors-green-300: #68D391;
        --chakra-colors-green-400: #48BB78;
        --chakra-colors-green-500: #38A169;
        --chakra-colors-green-600: #2F855A;
        --chakra-colors-green-700: #276749;
        --chakra-colors-green-800: #22543D;
        --chakra-colors-green-900: #1C4532;
        --chakra-colors-teal-50: #E6FFFA;
        --chakra-colors-teal-100: #B2F5EA;
        --chakra-colors-teal-200: #81E6D9;
        --chakra-colors-teal-300: #4FD1C5;
        --chakra-colors-teal-400: #38B2AC;
        --chakra-colors-teal-500: #319795;
        --chakra-colors-teal-600: #2C7A7B;
        --chakra-colors-teal-700: #285E61;
        --chakra-colors-teal-800: #234E52;
        --chakra-colors-teal-900: #1D4044;
        --chakra-colors-blue-50: #ebf8ff;
        --chakra-colors-blue-100: #bee3f8;
        --chakra-colors-blue-200: #90cdf4;
        --chakra-colors-blue-300: #63b3ed;
        --chakra-colors-blue-400: #4299e1;
        --chakra-colors-blue-500: #3182ce;
        --chakra-colors-blue-600: #2b6cb0;
        --chakra-colors-blue-700: #2c5282;
        --chakra-colors-blue-800: #2a4365;
        --chakra-colors-blue-900: #1A365D;
        --chakra-colors-cyan-50: #EDFDFD;
        --chakra-colors-cyan-100: #C4F1F9;
        --chakra-colors-cyan-200: #9DECF9;
        --chakra-colors-cyan-300: #76E4F7;
        --chakra-colors-cyan-400: #0BC5EA;
        --chakra-colors-cyan-500: #00B5D8;
        --chakra-colors-cyan-600: #00A3C4;
        --chakra-colors-cyan-700: #0987A0;
        --chakra-colors-cyan-800: #086F83;
        --chakra-colors-cyan-900: #065666;
        --chakra-colors-purple-50: #FAF5FF;
        --chakra-colors-purple-100: #E9D8FD;
        --chakra-colors-purple-200: #D6BCFA;
        --chakra-colors-purple-300: #B794F4;
        --chakra-colors-purple-400: #9F7AEA;
        --chakra-colors-purple-500: #805AD5;
        --chakra-colors-purple-600: #6B46C1;
        --chakra-colors-purple-700: #553C9A;
        --chakra-colors-purple-800: #44337A;
        --chakra-colors-purple-900: #322659;
        --chakra-colors-pink-50: #FFF5F7;
        --chakra-colors-pink-100: #FED7E2;
        --chakra-colors-pink-200: #FBB6CE;
        --chakra-colors-pink-300: #F687B3;
        --chakra-colors-pink-400: #ED64A6;
        --chakra-colors-pink-500: #D53F8C;
        --chakra-colors-pink-600: #B83280;
        --chakra-colors-pink-700: #97266D;
        --chakra-colors-pink-800: #702459;
        --chakra-colors-pink-900: #521B41;
        --chakra-colors-linkedin-50: #E8F4F9;
        --chakra-colors-linkedin-100: #CFEDFB;
        --chakra-colors-linkedin-200: #9BDAF3;
        --chakra-colors-linkedin-300: #68C7EC;
        --chakra-colors-linkedin-400: #34B3E4;
        --chakra-colors-linkedin-500: #00A0DC;
        --chakra-colors-linkedin-600: #008CC9;
        --chakra-colors-linkedin-700: #0077B5;
        --chakra-colors-linkedin-800: #005E93;
        --chakra-colors-linkedin-900: #004471;
        --chakra-colors-facebook-50: #E8F4F9;
        --chakra-colors-facebook-100: #D9DEE9;
        --chakra-colors-facebook-200: #B7C2DA;
        --chakra-colors-facebook-300: #6482C0;
        --chakra-colors-facebook-400: #4267B2;
        --chakra-colors-facebook-500: #385898;
        --chakra-colors-facebook-600: #314E89;
        --chakra-colors-facebook-700: #29487D;
        --chakra-colors-facebook-800: #223B67;
        --chakra-colors-facebook-900: #1E355B;
        --chakra-colors-messenger-50: #D0E6FF;
        --chakra-colors-messenger-100: #B9DAFF;
        --chakra-colors-messenger-200: #A2CDFF;
        --chakra-colors-messenger-300: #7AB8FF;
        --chakra-colors-messenger-400: #2E90FF;
        --chakra-colors-messenger-500: #0078FF;
        --chakra-colors-messenger-600: #0063D1;
        --chakra-colors-messenger-700: #0052AC;
        --chakra-colors-messenger-800: #003C7E;
        --chakra-colors-messenger-900: #002C5C;
        --chakra-colors-whatsapp-50: #dffeec;
        --chakra-colors-whatsapp-100: #b9f5d0;
        --chakra-colors-whatsapp-200: #90edb3;
        --chakra-colors-whatsapp-300: #65e495;
        --chakra-colors-whatsapp-400: #3cdd78;
        --chakra-colors-whatsapp-500: #22c35e;
        --chakra-colors-whatsapp-600: #179848;
        --chakra-colors-whatsapp-700: #0c6c33;
        --chakra-colors-whatsapp-800: #01421c;
        --chakra-colors-whatsapp-900: #001803;
        --chakra-colors-twitter-50: #E5F4FD;
        --chakra-colors-twitter-100: #C8E9FB;
        --chakra-colors-twitter-200: #A8DCFA;
        --chakra-colors-twitter-300: #83CDF7;
        --chakra-colors-twitter-400: #57BBF5;
        --chakra-colors-twitter-500: #1DA1F2;
        --chakra-colors-twitter-600: #1A94DA;
        --chakra-colors-twitter-700: #1681BF;
        --chakra-colors-twitter-800: #136B9E;
        --chakra-colors-twitter-900: #0D4D71;
        --chakra-colors-telegram-50: #E3F2F9;
        --chakra-colors-telegram-100: #C5E4F3;
        --chakra-colors-telegram-200: #A2D4EC;
        --chakra-colors-telegram-300: #7AC1E4;
        --chakra-colors-telegram-400: #47A9DA;
        --chakra-colors-telegram-500: #0088CC;
        --chakra-colors-telegram-600: #007AB8;
        --chakra-colors-telegram-700: #006BA1;
        --chakra-colors-telegram-800: #005885;
        --chakra-colors-telegram-900: #003F5E;
        --chakra-borders-none: 0;
        --chakra-borders-1px: 1px solid;
        --chakra-borders-2px: 2px solid;
        --chakra-borders-4px: 4px solid;
        --chakra-borders-8px: 8px solid;
        --chakra-fonts-heading: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
        --chakra-fonts-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
        --chakra-fonts-mono: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
        --chakra-fontSizes-3xs: 0.45rem;
        --chakra-fontSizes-2xs: 0.625rem;
        --chakra-fontSizes-xs: 0.75rem;
        --chakra-fontSizes-sm: 0.875rem;
        --chakra-fontSizes-md: 1rem;
        --chakra-fontSizes-lg: 1.125rem;
        --chakra-fontSizes-xl: 1.25rem;
        --chakra-fontSizes-2xl: 1.5rem;
        --chakra-fontSizes-3xl: 1.875rem;
        --chakra-fontSizes-4xl: 2.25rem;
        --chakra-fontSizes-5xl: 3rem;
        --chakra-fontSizes-6xl: 3.75rem;
        --chakra-fontSizes-7xl: 4.5rem;
        --chakra-fontSizes-8xl: 6rem;
        --chakra-fontSizes-9xl: 8rem;
        --chakra-fontWeights-hairline: 100;
        --chakra-fontWeights-thin: 200;
        --chakra-fontWeights-light: 300;
        --chakra-fontWeights-normal: 400;
        --chakra-fontWeights-medium: 500;
        --chakra-fontWeights-semibold: 600;
        --chakra-fontWeights-bold: 700;
        --chakra-fontWeights-extrabold: 800;
        --chakra-fontWeights-black: 900;
        --chakra-letterSpacings-tighter: -0.05em;
        --chakra-letterSpacings-tight: -0.025em;
        --chakra-letterSpacings-normal: 0;
        --chakra-letterSpacings-wide: 0.025em;
        --chakra-letterSpacings-wider: 0.05em;
        --chakra-letterSpacings-widest: 0.1em;
        --chakra-lineHeights-3: .75rem;
        --chakra-lineHeights-4: 1rem;
        --chakra-lineHeights-5: 1.25rem;
        --chakra-lineHeights-6: 1.5rem;
        --chakra-lineHeights-7: 1.75rem;
        --chakra-lineHeights-8: 2rem;
        --chakra-lineHeights-9: 2.25rem;
        --chakra-lineHeights-10: 2.5rem;
        --chakra-lineHeights-normal: normal;
        --chakra-lineHeights-none: 1;
        --chakra-lineHeights-shorter: 1.25;
        --chakra-lineHeights-short: 1.375;
        --chakra-lineHeights-base: 1.5;
        --chakra-lineHeights-tall: 1.625;
        --chakra-lineHeights-taller: 2;
        --chakra-radii-none: 0;
        --chakra-radii-sm: 0.125rem;
        --chakra-radii-base: 0.25rem;
        --chakra-radii-md: 0.375rem;
        --chakra-radii-lg: 0.5rem;
        --chakra-radii-xl: 0.75rem;
        --chakra-radii-2xl: 1rem;
        --chakra-radii-3xl: 1.5rem;
        --chakra-radii-full: 9999px;
        --chakra-space-1: 0.25rem;
        --chakra-space-2: 0.5rem;
        --chakra-space-3: 0.75rem;
        --chakra-space-4: 1rem;
        --chakra-space-5: 1.25rem;
        --chakra-space-6: 1.5rem;
        --chakra-space-7: 1.75rem;
        --chakra-space-8: 2rem;
        --chakra-space-9: 2.25rem;
        --chakra-space-10: 2.5rem;
        --chakra-space-12: 3rem;
        --chakra-space-14: 3.5rem;
        --chakra-space-16: 4rem;
        --chakra-space-20: 5rem;
        --chakra-space-24: 6rem;
        --chakra-space-28: 7rem;
        --chakra-space-32: 8rem;
        --chakra-space-36: 9rem;
        --chakra-space-40: 10rem;
        --chakra-space-44: 11rem;
        --chakra-space-48: 12rem;
        --chakra-space-52: 13rem;
        --chakra-space-56: 14rem;
        --chakra-space-60: 15rem;
        --chakra-space-64: 16rem;
        --chakra-space-72: 18rem;
        --chakra-space-80: 20rem;
        --chakra-space-96: 24rem;
        --chakra-space-px: 1px;
        --chakra-space-0-5: 0.125rem;
        --chakra-space-1-5: 0.375rem;
        --chakra-space-2-5: 0.625rem;
        --chakra-space-3-5: 0.875rem;
        --chakra-shadows-xs: 0 0 0 1px rgba(0, 0, 0, 0.05);
        --chakra-shadows-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
        --chakra-shadows-base: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
        --chakra-shadows-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
        --chakra-shadows-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
        --chakra-shadows-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
        --chakra-shadows-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
        --chakra-shadows-outline: 0 0 0 3px rgba(66, 153, 225, 0.6);
        --chakra-shadows-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
        --chakra-shadows-none: none;
        --chakra-shadows-dark-lg: rgba(0, 0, 0, 0.1) 0px 0px 0px 1px, rgba(0, 0, 0, 0.2) 0px 5px 10px, rgba(0, 0, 0, 0.4) 0px 15px 40px;
        --chakra-sizes-1: 0.25rem;
        --chakra-sizes-2: 0.5rem;
        --chakra-sizes-3: 0.75rem;
        --chakra-sizes-4: 1rem;
        --chakra-sizes-5: 1.25rem;
        --chakra-sizes-6: 1.5rem;
        --chakra-sizes-7: 1.75rem;
        --chakra-sizes-8: 2rem;
        --chakra-sizes-9: 2.25rem;
        --chakra-sizes-10: 2.5rem;
        --chakra-sizes-12: 3rem;
        --chakra-sizes-14: 3.5rem;
        --chakra-sizes-16: 4rem;
        --chakra-sizes-20: 5rem;
        --chakra-sizes-24: 6rem;
        --chakra-sizes-28: 7rem;
        --chakra-sizes-32: 8rem;
        --chakra-sizes-36: 9rem;
        --chakra-sizes-40: 10rem;
        --chakra-sizes-44: 11rem;
        --chakra-sizes-48: 12rem;
        --chakra-sizes-52: 13rem;
        --chakra-sizes-56: 14rem;
        --chakra-sizes-60: 15rem;
        --chakra-sizes-64: 16rem;
        --chakra-sizes-72: 18rem;
        --chakra-sizes-80: 20rem;
        --chakra-sizes-96: 24rem;
        --chakra-sizes-px: 1px;
        --chakra-sizes-0-5: 0.125rem;
        --chakra-sizes-1-5: 0.375rem;
        --chakra-sizes-2-5: 0.625rem;
        --chakra-sizes-3-5: 0.875rem;
        --chakra-sizes-max: max-content;
        --chakra-sizes-min: min-content;
        --chakra-sizes-full: 100%;
        --chakra-sizes-3xs: 14rem;
        --chakra-sizes-2xs: 16rem;
        --chakra-sizes-xs: 20rem;
        --chakra-sizes-sm: 24rem;
        --chakra-sizes-md: 28rem;
        --chakra-sizes-lg: 32rem;
        --chakra-sizes-xl: 36rem;
        --chakra-sizes-2xl: 42rem;
        --chakra-sizes-3xl: 48rem;
        --chakra-sizes-4xl: 56rem;
        --chakra-sizes-5xl: 64rem;
        --chakra-sizes-6xl: 72rem;
        --chakra-sizes-7xl: 80rem;
        --chakra-sizes-8xl: 90rem;
        --chakra-sizes-prose: 60ch;
        --chakra-sizes-container-sm: 640px;
        --chakra-sizes-container-md: 768px;
        --chakra-sizes-container-lg: 1024px;
        --chakra-sizes-container-xl: 1280px;
        --chakra-zIndices-hide: -1;
        --chakra-zIndices-auto: auto;
        --chakra-zIndices-base: 0;
        --chakra-zIndices-docked: 10;
        --chakra-zIndices-dropdown: 1000;
        --chakra-zIndices-sticky: 1100;
        --chakra-zIndices-banner: 1200;
        --chakra-zIndices-overlay: 1300;
        --chakra-zIndices-modal: 1400;
        --chakra-zIndices-popover: 1500;
        --chakra-zIndices-skipLink: 1600;
        --chakra-zIndices-toast: 1700;
        --chakra-zIndices-tooltip: 1800;
        --chakra-transition-property-common: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;
        --chakra-transition-property-colors: background-color, border-color, color, fill, stroke;
        --chakra-transition-property-dimensions: width, height;
        --chakra-transition-property-position: left, right, top, bottom;
        --chakra-transition-property-background: background-color, background-image, background-position;
        --chakra-transition-easing-ease-in: cubic-bezier(0.4, 0, 1, 1);
        --chakra-transition-easing-ease-out: cubic-bezier(0, 0, 0.2, 1);
        --chakra-transition-easing-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
        --chakra-transition-duration-ultra-fast: 50ms;
        --chakra-transition-duration-faster: 100ms;
        --chakra-transition-duration-fast: 150ms;
        --chakra-transition-duration-normal: 200ms;
        --chakra-transition-duration-slow: 300ms;
        --chakra-transition-duration-slower: 400ms;
        --chakra-transition-duration-ultra-slow: 500ms;
        --chakra-blur-none: 0;
        --chakra-blur-sm: 4px;
        --chakra-blur-base: 8px;
        --chakra-blur-md: 12px;
        --chakra-blur-lg: 16px;
        --chakra-blur-xl: 24px;
        --chakra-blur-2xl: 40px;
        --chakra-blur-3xl: 64px;
        --chakra-breakpoints-base: 0em;
        --chakra-breakpoints-sm: 30em;
        --chakra-breakpoints-md: 48em;
        --chakra-breakpoints-lg: 62em;
        --chakra-breakpoints-xl: 80em;
        --chakra-breakpoints-2xl: 96em;
    }

    .chakra-ui-light :host:not([data-theme]), .chakra-ui-light :root:not([data-theme]), .chakra-ui-light [data-theme]:not([data-theme]), [data-theme=light] :host:not([data-theme]), [data-theme=light] :root:not([data-theme]), [data-theme=light] [data-theme]:not([data-theme]), :host[data-theme=light], :root[data-theme=light], [data-theme][data-theme=light] {
        --chakra-colors-chakra-body-text: var(--chakra-colors-gray-800);
        --chakra-colors-chakra-body-bg: var(--chakra-colors-white);
        --chakra-colors-chakra-border-color: var(--chakra-colors-gray-200);
        --chakra-colors-chakra-inverse-text: var(--chakra-colors-white);
        --chakra-colors-chakra-subtle-bg: var(--chakra-colors-gray-100);
        --chakra-colors-chakra-subtle-text: var(--chakra-colors-gray-600);
        --chakra-colors-chakra-placeholder-color: var(--chakra-colors-gray-500);
    }

    .chakra-ui-dark :host:not([data-theme]), .chakra-ui-dark :root:not([data-theme]), .chakra-ui-dark [data-theme]:not([data-theme]), [data-theme=dark] :host:not([data-theme]), [data-theme=dark] :root:not([data-theme]), [data-theme=dark] [data-theme]:not([data-theme]), :host[data-theme=dark], :root[data-theme=dark], [data-theme][data-theme=dark] {
        --chakra-colors-chakra-body-text: var(--chakra-colors-whiteAlpha-900);
        --chakra-colors-chakra-body-bg: var(--chakra-colors-gray-800);
        --chakra-colors-chakra-border-color: var(--chakra-colors-whiteAlpha-300);
        --chakra-colors-chakra-inverse-text: var(--chakra-colors-gray-800);
        --chakra-colors-chakra-subtle-bg: var(--chakra-colors-gray-700);
        --chakra-colors-chakra-subtle-text: var(--chakra-colors-gray-400);
        --chakra-colors-chakra-placeholder-color: var(--chakra-colors-whiteAlpha-400);
    }</style>
    <style data-emotion="css-global fubdgu">html {
        line-height: 1.5;
        -webkit-text-size-adjust: 100%;
        font-family: system-ui, sans-serif;
        -webkit-font-smoothing: antialiased;
        text-rendering: optimizeLegibility;
        -moz-osx-font-smoothing: grayscale;
        touch-action: manipulation;
    }

    body {
        position: relative;
        min-height: 100%;
        margin: 0;
        font-feature-settings: "kern";
    }

    :where(*, *::before, *::after) {
        border-width: 0;
        border-style: solid;
        box-sizing: border-box;
        word-wrap: break-word;
    }

    main {
        display: block;
    }

    hr {
        border-top-width: 1px;
        box-sizing: content-box;
        height: 0;
        overflow: visible;
    }

    :where(pre, code, kbd,samp) {
        font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace;
        font-size: 1em;
    }

    a {
        background-color: transparent;
        color: inherit;
        -webkit-text-decoration: inherit;
        text-decoration: inherit;
    }

    abbr[title] {
        border-bottom: none;
        -webkit-text-decoration: underline;
        text-decoration: underline;
        -webkit-text-decoration: underline dotted;
        -webkit-text-decoration: underline dotted;
        text-decoration: underline dotted;
    }

    :where(b, strong) {
        font-weight: bold;
    }

    small {
        font-size: 80%;
    }

    :where(sub,sup) {
        font-size: 75%;
        line-height: 0;
        position: relative;
        vertical-align: baseline;
    }

    sub {
        bottom: -0.25em;
    }

    sup {
        top: -0.5em;
    }

    img {
        border-style: none;
    }

    :where(button, input, optgroup, select, textarea) {
        font-family: inherit;
        font-size: 100%;
        line-height: 1.15;
        margin: 0;
    }

    :where(button, input) {
        overflow: visible;
    }

    :where(button, select) {
        text-transform: none;
    }

    :where(
          button::-moz-focus-inner,
          [type="button"]::-moz-focus-inner,
          [type="reset"]::-moz-focus-inner,
          [type="submit"]::-moz-focus-inner
        ) {
        border-style: none;
        padding: 0;
    }

    fieldset {
        padding: 0.35em 0.75em 0.625em;
    }

    legend {
        box-sizing: border-box;
        color: inherit;
        display: table;
        max-width: 100%;
        padding: 0;
        white-space: normal;
    }

    progress {
        vertical-align: baseline;
    }

    textarea {
        overflow: auto;
    }

    :where([type="checkbox"], [type="radio"]) {
        box-sizing: border-box;
        padding: 0;
    }

    input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button {
        -webkit-appearance: none !important;
    }

    input[type="number"] {
        -moz-appearance: textfield;
    }

    input[type="search"] {
        -webkit-appearance: textfield;
        outline-offset: -2px;
    }

    input[type="search"]::-webkit-search-decoration {
        -webkit-appearance: none !important;
    }

    ::-webkit-file-upload-button {
        -webkit-appearance: button;
        font: inherit;
    }

    details {
        display: block;
    }

    summary {
        display: -webkit-box;
        display: -webkit-list-item;
        display: -ms-list-itembox;
        display: list-item;
    }

    template {
        display: none;
    }

    [hidden] {
        display: none !important;
    }

    :where(
          blockquote,
          dl,
          dd,
          h1,
          h2,
          h3,
          h4,
          h5,
          h6,
          hr,
          figure,
          p,
          pre
        ) {
        margin: 0;
    }

    button {
        background: transparent;
        padding: 0;
    }

    fieldset {
        margin: 0;
        padding: 0;
    }

    :where(ol, ul) {
        margin: 0;
        padding: 0;
    }

    textarea {
        resize: vertical;
    }

    :where(button, [role="button"]) {
        cursor: pointer;
    }

    button::-moz-focus-inner {
        border: 0 !important;
    }

    table {
        border-collapse: collapse;
    }

    :where(h1, h2, h3, h4, h5, h6) {
        font-size: inherit;
        font-weight: inherit;
    }

    :where(button, input, optgroup, select, textarea) {
        padding: 0;
        line-height: inherit;
        color: inherit;
    }

    :where(img, svg, video, canvas, audio, iframe, embed, object) {
        display: block;
    }

    :where(img, video) {
        max-width: 100%;
        height: auto;
    }

    [data-js-focus-visible] :focus:not([data-focus-visible-added]):not(
          [data-focus-visible-disabled]
        ) {
        outline: none;
        box-shadow: none;
    }

    select::-ms-expand {
        display: none;
    }

    :root, :host {
        --chakra-vh: 100vh;
    }

    @supports (height: -webkit-fill-available) {
        :root, :host {
            --chakra-vh: -webkit-fill-available;
        }
    }

    @supports (height: -moz-fill-available) {
        :root, :host {
            --chakra-vh: -moz-fill-available;
        }
    }

    @supports (height: 100dvh) {
        :root, :host {
            --chakra-vh: 100dvh;
        }
    }</style>
    <style data-emotion="css-global 1cgn62j">body {
        font-family: var(--chakra-fonts-body);
        color: var(--chakra-colors-chakra-body-text);
        background: var(--chakra-colors-chakra-body-bg);
        transition-property: background-color;
        transition-duration: var(--chakra-transition-duration-normal);
        line-height: var(--chakra-lineHeights-base);
    }

    *::-webkit-input-placeholder {
        color: var(--chakra-colors-chakra-placeholder-color);
    }

    *::-moz-placeholder {
        color: var(--chakra-colors-chakra-placeholder-color);
    }

    *:-ms-input-placeholder {
        color: var(--chakra-colors-chakra-placeholder-color);
    }

    *::placeholder {
        color: var(--chakra-colors-chakra-placeholder-color);
    }

    *, *::before, ::after {
        border-color: var(--chakra-colors-chakra-border-color);
    }</style>
    <style data-emotion="css-global 8gzb3n">.js-focus-visible :focus:not([data-focus-visible-added]) {
        outline: none;
        box-shadow: none;
    }</style>
    <style data-emotion="css 1dklj6k">.css-1dklj6k {
        font-family: var(--chakra-fonts-heading);
        font-weight: var(--chakra-fontWeights-bold);
        font-size: var(--chakra-fontSizes-3xl);
        line-height: 1.33;
    }

    @media screen and (min-width: 48em) {
        .css-1dklj6k {
            font-size: var(--chakra-fontSizes-4xl);
            line-height: 1.2;
        }
    }</style>
    <h2 class="chakra-heading css-1dklj6k">Hello world!</h2><span></span><span id="__chakra_env" hidden=""></span></div>
<script src="/_next/static/chunks/react-refresh.js?ts=1696314172501"></script>
<script id="__NEXT_DATA__" type="application/json">{
    "props": {
        "pageProps": {}
    },
    "page": "/",
    "query": {},
    "buildId": "development",
    "nextExport": true,
    "autoExport": true,
    "isFallback": false,
    "scriptLoader": []
}</script>
</body>
</html>

하여간 굉장히 길다.
밑에서 16번째 줄만 읽어보면

<h2 class="chakra-heading css-1dklj6k">Hello world!</h2>

라는 라인과 그 안에 "Hello world!"가 보인다.

 

이 긴 코드들을 이해하려고 시도하는 것은
(개인적으로는) 리플렉스 제작자의 의도와 벗어난 것이라고 생각한다.
그저, 리플렉스 사용자가 페이지와 로직만 구현하면 되게끔
모든 부수적인 코드를 미리 구현해놓았다고 받아들이면 좋겠다.

다시 본론으로 돌아와서,

위에서 파이썬으로 구현한 라인은 "Hello world!"라는 제목을 브라우저에 표시하는 코드이다.
index 함수를 보면 특별한 내용 없이 rx.heading이라는 컴포넌트 하나를 리턴했는데
이 리턴한 컴포넌트가 곧 html 페이지가 된다.
이번엔 heading 컴포넌트 아래에 일반 text 컴포넌트를 하나만 더 추가해보자.
리플렉스 원칙상 하나의 페이지 함수(예:index)는 하나의 컴포넌트만 리턴해야 한다.
그래서 두 개 이상의 컴포넌트를 한 페이지에 조합하기 위해서는
여러 개의 컴포넌트를 담을 수 있는 레이아웃 컴포넌트란 게 필요하다.

이번에는 간단히 rx.container라는 컨테이너 컴포넌트를 사용해서
rx.heading과 rx.text를 담아보자.

import reflex as rx


def index():
    return rx.container(
        rx.heading("Hello world!"),
        rx.text("This is reflex.")
    )


app = rx.App()
app.add_page(index)
app.compile()

 코드를 위와 같이 작성하고
터미널에서 `reflex run`을 실행하면
브라우저의 localhost:3000으로 아래와 같은 화면을 확인할 수 있다.

 

이렇게 간단한 페이지를 만들어보았다.
그런데 리플렉스는 이렇게 간단한 정적 페이지를 만드는 용도로 사용하기엔
내재된 기능이 참 다양하고,
특히 다소 복잡한 백엔드 로직을 구현하는 방법도
대부분의 경우 굉장히 간단히 파이썬 코드로 구현이 가능하다.
이론적으로는, React와 FastAPI로 구현할 수 있는 모든 프론트/백엔드 로직 구현이 가능하다.
다음 포스팅에서는 본격적으로 State 클래스와 몇 가지 prop 속성을 추가하면서
간단한 동적 웹앱을 하나 만들어보자.

+버튼과 -버튼으로 특정 숫자를 올리고 내리는
일명 Counter 앱을 만들어보고자 한다.

 

댓글