본문 바로가기
REFLEX 튜토리얼

[pynecone] Dall-E 2 이미지 생성 앱 만들기④ #State

by 일코 2023. 2. 16.

들어가기 전에

지난 시간에는
작정하고 우리 앱에 들어가는 파인콘 컴포넌트 10개를
모두 짚어보았습니다.

사실 컴포넌트가 어떻게 작동하고
어떤 속성을 지녔는지를 파악했다면,
이들을 배치하고 조합하는 코딩은
듀플로를 조립하듯 너무 간단한 일입니다.

Lego Duplo

기억이 나지 않으면 검색하면 되고요.
https://pynecone.io/docs/library 알려드렸죠?

그래서
프론트엔드 중에서도 UI 파트라고 할 수 있는
index 함수를 짜는 것보다
더 중요한 것은
웹앱의 상태값(변수 등)을 저장하는
State클래스에 대해 이해하는 것입니다.
지난 튜토리얼에서도 계속해서 State 클래스와 State 변수에 대해 설명드렸지만
가장 중요한 점은
State 클래스 안에 정의된 변수들은 특별한 기능을 지녔다는 것입니다.
바로 State 변수가 바뀌는 것을 파인콘이(정확히는 내부의 리액트가) 감지해서
화면을 바꿔주기 때문입니다. (렌더링한다고도 합니다.)

State클래스가 아닌 index 함수 안에 변수를 선언해버리면
새로고침 등 매번 index를 호출할 때마다
변수의 현재값이 초기화되어버릴 것입니다.
index는 함수이기 때문이죠.
그래서 값이 변경되고, 또 저장되어야 하는 변수들은
pc.State를 상속받은 State 클래스 안에서만 정의되어야 합니다.

리액트를 조금이라도 접해 보신 분들은
State나 Hook의 기능에 대해 알고 계시리라 생각합니다.
이 튜토리얼은 리액트에 대한 사전지식이 전혀 없어도
리액트 웹앱을 만들 수 있게 하는 것을 목적으로 합니다.

그럼 본격적으로 
이미지 생성 앱을 만들어봅시다.

우선 우리 dall_e.py 파일의 상태는 아래와 같습니다.

import openai
import pynecone as pc

openai.api_key = "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"  # 여러분의 API키로 교체


class State(pc.State):
    pass


def index():
    return


app = pc.App(state=State)
app.add_page(index, title="Pynecone:DALL-E2")
app.compile()

우선 index 함수 안에다가
pc.heading, pc.input, pc.button, pc.divider, pc.circular_progress, pc.image를
순서대로 pc.vstack 안에 담고, pc.center로 감싸서 화면 가운데 위치시켜봅시다.

index 함수는 아래와 같이 작성하면 됩니다.

def index():
    return pc.center(
        pc.vstack(
            pc.heading("DALL-E", font_size="1.5em"),
            pc.input(placeholder="Enter a prompt.."),
            pc.button("Generate Image", width="100%"),
            pc.divider(),
            pc.circular_progress(is_indeterminate=True),
            pc.image(src="https://blog.kakaocdn.net/dn/dECMql/btrYpnRHYLF/NANk86DANrdoLleGNcTFF0/img.jpg", width="20em"),
            bg="white", padding="2em", shadow="lg", border_radius="lg",
        ),
        width="100%", height="100vh",
        background="radial-gradient(circle at 22% 11%,rgba(62, 180, 137,.20),hsla(0,0%,100%,0) 19%),radial-gradient(circle at 82% 25%,rgba(33,150,243,.18),hsla(0,0%,100%,0) 35%),radial-gradient(circle at 25% 61%,rgba(250, 128, 114, .28),hsla(0,0%,100%,0) 55%)",
    )

 

그럼 아래와 같은 레이아웃이 완성됩니다.

아직 아무런 동적 기능은 들어있지 않은 상태입니다.

pc.circular_progress와 pc.image는
다음 포스팅에서 각각 pc.cond 안에 넣어서 감출 예정이에요.
pc.circular_progress는 로딩시에만,
pc.image는 로딩 완료시에만 나타나게 말이죠.

벌써 프로그램의 레이아웃이 완성되어버렸습니다.

 

이제 State 클래스를 정의할 차례입니다.

위에서도 말씀드렸다시피,
특정 값을 저장하거나,
값에 따라 프로그램의 화면이 업데이트되어야 한다면
State 안에 해당 값을 변수로 정의해줘야 합니다.

우리 프로그램에서 
그런 기능이 필요한 변수가 어떤 게 있을지 생각해봅시다.

1. 사용자로부터 pc.input을 통해 입력받을 프롬프트값 : prompt
2. openai API가 작성한 이미지의 주소값 : image_url
3. 이미지 로딩중 여부(버튼 클릭 시점부터 이미지 로딩완료 시점까지만 pc.circular_progress를 나타나게)
4. 이미지 로딩완료 여부(pc.image 컴포넌트를 숨겨뒀다가 로딩이 완료되면 나타나게)

이렇게 네 개 정도면 될 것 같은데요? (더 있나요?^^;)

그럼 위 네 가지 정보를 토대로 State 클래스를 정의해봅시다.

class State(pc.State):
    prompt = ""
    image_url = ""
    진행중 = False
    완료 = False

간단하게 State를 정의했습니다.
물론 완성된 건 아니지만,
추가할 것이 딱 한 가지인데
pc.button 클릭시에 on_click 트리거를 통해
State.진행중 변수와 State.완료 변수를 변경하기 위해서는
간단한 함수를 만들어 추가해 둬야겠죠?

이쯤에서 (별로 한 건 없지만)

우리 dall_e.py의 전체 코드를 한 번 살펴볼까요?

import openai
import pynecone as pc

openai.api_key = "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"  # 여러분의 API키로 교체


class State(pc.State):
    prompt = ""
    image_url = ""
    진행중 = False
    완료 = False

    
def index():
    return pc.center(
        pc.vstack(
            pc.heading("DALL-E", font_size="1.5em"),
            pc.input(placeholder="Enter a prompt.."),
            pc.button("Generate Image", width="100%"),
            pc.divider(),
            pc.circular_progress(is_indeterminate=True),
            pc.image(src="https://blog.kakaocdn.net/dn/dECMql/btrYpnRHYLF/NANk86DANrdoLleGNcTFF0/img.jpg", width="20em"),
            bg="white", padding="2em", shadow="lg", border_radius="lg",
        ),
        width="100%", height="100vh",
        background="radial-gradient(circle at 22% 11%,rgba(62, 180, 137,.20),hsla(0,0%,100%,0) 19%),radial-gradient(circle at 82% 25%,rgba(33,150,243,.18),hsla(0,0%,100%,0) 35%),radial-gradient(circle at 25% 61%,rgba(250, 128, 114, .28),hsla(0,0%,100%,0) 55%)",
    )


app = pc.App(state=State)
app.add_page(index, title="Pynecone:DALL-E")
app.compile()

 

그래도 제법 코드 길이가 길어졌네요.

이번에는
State.prompt의 값을
pc.input으로 입력받는 값으로 변경하는 코드를 작성해봅시다.

특정 컴포넌트 안에 이벤트를 생성할 때에는
어떤 트리거랑 붙여야 가장 효율적일지 잠깐 고민해봐야 합니다.
pc.input에 입력할 때(매번 타이핑할 때) 말고
입력완료 후 pc.input의 포커스가 버튼이나 다른 컴포넌트로 옮겨갈 때(on_blur)
State.prompt를 변경하는 게 가장 좋을 것 같습니다.

(위의 이유로) pc.input의 on_blur 파라미터로
State.prompt를 변경하는 이벤트를 추가해주겠습니다.
pc.input을 아래와 같이 수정해줍니다.

pc.input(placeholder="Enter a prompt..", on_blur=State.set_prompt),
#                                        ------------------------
'어? 나는 State 클래스에 set_prompt라는 걸 정의한 적이 없는데?'

하고 생각하시는 분이 있을 것으로 생각합니다.
지난 포스팅에서 언급한 바 있지만,
pc.State에서 제공하는 기능 중 하나로
State 클래스 안에서는
클래스 안에 정의된 변수의 값을 바꿔주는
"set_변수명()" 이라는 함수를
자동으로 생성해줍니다.

(아직 눈으로 확인하지는 못했지만..)
set_prompt를 통해 State.prompt의 값이 바뀌게 되었습니다.

그럼 이번에는 버튼을 클릭할 때
openai.com에 State.prompt를 전송하는 코드를 작성해보겠습니다.

먼저 State 클래스 하단에
아래 메서드를 추가해줍니다.

# ...

def get_image(self):
    response = openai.Image.create(prompt=self.prompt, n=1, size="256x256")
    self.image_url = response["data"][0]["url"]

 

그리고 index 함수의 pc.button도 아래와 같이 수정해줍니다.

pc.button("Generate Image", on_click=State.get_image, width="100%")
#                           -------------------------

 

마지막으로 pc.image의 src도 image_url로 수정해줍시다.

pc.image(src=State.image_url, width="20em"),
#            ----------------

 

이쯤 되면 한 번 실행해보고싶어지지 않나요?
(완성되지는 않았지만) 빠진 건 없는지 아래코드랑 비교점검하고,

import openai
import pynecone as pc

openai.api_key = "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"


class State(pc.State):
    prompt = ""
    image_url = ""
    진행중 = False
    완료 = False

    def get_image(self):
        response = openai.Image.create(prompt=self.prompt, n=1, size="256x256")
        self.image_url = response["data"][0]["url"]
        self.완료 = True


def index():
    return pc.center(
        pc.vstack(
            pc.heading("DALL-E", font_size="1.5em"),
            pc.input(placeholder="Enter a prompt..", on_blur=State.set_prompt),
            pc.button("Generate Image", width="100%", on_click=State.get_image),
            pc.divider(),
            pc.circular_progress(is_indeterminate=True),
            pc.cond(State.완료, pc.image(src=State.image_url, width="20em")),
            bg="white", padding="2em", shadow="lg", border_radius="lg",
        ),
        width="100%", height="100vh",
        background="radial-gradient(circle at 22% 11%,rgba(62, 180, 137,.20),hsla(0,0%,100%,0) 19%),radial-gradient(circle at 82% 25%,rgba(33,150,243,.18),hsla(0,0%,100%,0) 35%),radial-gradient(circle at 25% 61%,rgba(250, 128, 114, .28),hsla(0,0%,100%,0) 55%)",
    )


app = pc.App(state=State)
app.add_page(index, title="Pynecone:DALL-E")
app.compile()

 

부족한대로 이 코드대로 실행을 해봅시다.
터미널에서 "pc run"을 입력하고 엔터.

파이참에서는 Alt-F12가 터미널 단축키입니다.

이제 브라우저를 열고 localhost:3000 으로 접속한 후 프롬프트를 하나 날려봅시다.
(터미널 메시지의 http://localhost:3000 링크를 클릭하셔도 됩니다.)

프로그레스바가 눈에 거슬리는데, 바로 아래에서 pc.cond로 없애줄 겁니다.

 

뭐, 그런대로 쓸만한 웹앱이 되었네요!
이번 포스팅은 여기서 마치고
다음 포스팅에서 튜토리얼 마무리를 짓겠습니다.

다음 포스팅에서 해 볼 것은
딱 두 가지입니다.

1. 프로그레스바를 적절한 타이밍에만 나타나게 변경하기
2. 이미지를 8개 정도 동시에 출력하게 하기

이 정도면 우리 앱에도
손색이 없을 것 같습니다.

그럼 다음 포스팅에서 뵙겠습니다!

2023.02.16 - [pynecone 튜토리얼] - [pynecone] Dall-E 2 이미지 생성 앱 만들기⑤

 

[pynecone] Dall-E 2 이미지 생성 앱 만들기⑤

파인콘으로 이미지생성앱 만들기 마지막 시간입니다. 지난 포스팅에서 사실상 앱을 완성한 거나 다름이 없었는데, 2023.02.16 - [pynecone 튜토리얼] - [pynecone] Dall-E 2 이미지 생성 앱 만들기④ [pynecone]

martinii.fun

 

댓글