언어 공부

[week8] Langchain을 활용한 자동 블로그 글 작성 (feat.Nano Banana)

mapsycoy 2025. 8. 20. 03:27

나는 비개발자이지만, 요즘은 시간이 날 때마다 조금씩 Python을 공부해보려고 한다.
그래서 이번 포스팅에서는 Python 공부 목적으로 Langchain을 활용하여 LLM이 사전에 학습된 나만의 문체로 블로그 글을 작성하도록 해볼 예정이다.

Understanding LangChain - A Framework for LLM Application @ProjectPro

LangChain은 AI 개발에 필요한 요소들을 마치 체인처럼 하나로 연결시켜주는 프레임워크이다.

공식 로고

앵무새와 체인 이미지가 로고이다보니 대부분이 Language와 Chain의 합성어라고 보고 있지만, 이에 대한 공식 문서는 없다.

공식지원 언어는 파이썬과 자바스크립트이다.


[목차여기]

구현 방법 및 설명

준비: ComfyUI 사용자라면 Python 설치는 기본 중에 기본!

가상 환경 구축

원하는 위치에 새 폴더(ex. Auto_Blog)를 생성 후, 해당 경로에서 cmd를 켜고 가상 환경을 구축하자.

일회성 테스트 목적이기 때문에 나는 다운로드 경로에 생성하였다.

py -m venv .venv #-m venv: Python의 모듈(-m) 옵션을 사용하여 venv 모듈을 실행
.venv\Scripts\activate #가상 환경을 활성화하기 위한 스크립트를 실행
  • venv: Python에 내장된 가상 환경 생성 모듈
pip install -U langchain langchain-community langchain-openai langgraph \
              pydantic python-dotenv tiktoken faiss-cpu beautifulsoup4 requests \
              tavily-python langchain-tavily
  • pip: Python의 패키지 관리 도구
  • -U: 이미 설치된 패키지를 최신 버전으로 업그레이드
  • langchain: 대형 언어 모델(LLM)을 활용한 애플리케이션 개발을 위한 프레임워크
  • langchain-community: LangChain의 기본 기능 외에 커뮤니티에서 제공하는 확장된 통합 기능 사용
  • langchain-openai: OpenAI의 LLM을 LangChain 기반 애플리케이션에 통합
  • langgraph: LangChain 생태계에서 복잡한 워크플로우를 그래프 구조로 관리하기 위한 라이브러리
  • pydantic: Python에서 데이터 검증과 설정 관리를 위한 라이브러리
  • phython-dotenv: .env 파일에서 환경 변수를 로드하는 라이브러리 (.env: 환경 변수 저장 및 관리 목적의 텍스트 파일, 주로 API 키와 같은 민감 정보를 저장하는데 사용됨)
  • tiktoken: OpenAI에서 제공하는 토큰화 라이브러리
  • faiss-cpu: Facebook AI에서 개발한 벡터 검색 및 유사도 검색 라이브러리(CPU 버전) / GPU버전도 있음
  • beautifulsoup4: 웹 페이지에서 데이터를 크롤링하거나 구조화된 데이터를 추출하기 위한 라이브러리
  • equests: HTTP 요청을 보내기 위한 Python 라이브러리
  • tavily-python: Tavily 검색 API를 Python에서 사용하기 위한 라이브러리 (웹 검색용)
  • langchain-tavily: langchain 측에서 기존 TavilySearchResults라는 내장 클래스에서 별도로 분리시킨 패키지. LangChain 0.3.25 버전부터 이 클래스가 더 이상 유지보수되지 않고, LangChain 1.0에서 완전히 제거될 예정이라고 함.

API Key 등록

가상 환경 구축이 완료되면 폴더 속에 [.venv]폴더와 하위 폴더가 생긴다.

OPENAI_API_KEY=sk-... #OpenAI 키 삽입
TAVILY_API_KEY=tvly-... #Tavily(웹검색) API 키 삽입

[.venv]폴더로 넘어가지 않은 상태에서 [.env] 텍스트 파일을 새로 만들어 위처럼 두 개의 API키를 기록한다.

나는 블로그 글을 작성할 때 항상 인용이나 주석, 근거자료 출처 명시를 중요시 여기기 때문에, 외부 검색이 무조건 가능해야 한다. 때문에 파일 내에 [Tavily 검색 API 키]를 삽입한 것이다.

그리고 .txt 확장자를 제거하면, AnySign4PC 암호화 파일로 파일 유형이 변경될 것이다.


나만의 문체 학습

다음은 폴더에 [data/posts/] 경로를 생성 후, 나의 블로그 글을 해당 경로에 [.md]나 [.txt]로 넣는다.

가상 쉬운 방법은 마크다운 모드에서 복붙하는 것이다.

notepad C:\Users\mapsycoy\Downloads\Auto_Blog\style_extractor.py

이제 가상 환경 활성화 상태를 유지한 채로 위 스크립트를 실행하여 메모장을 켜고 style_extractor.py를 구축한다.

import glob, json, os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain.prompts import PromptTemplate

load_dotenv()

STYLE_EXTRACT_TMPL = """너는 문체 분석가다.
다음 글들을 분석해 'Mapsycoy 문체 규약' JSON을 생성하라.
요구:
- voice: 한줄 요약(시니컬 유머, 직설, 예의 등)
- do_list: 반드시 지킬 규칙 6~10개
- dont_list: 피할 것 4~8개
- humor: 유머 사용 가이드
- phrasing: 자주 쓰는 표현
- formatting: 헤딩/불릿/인용/코드블럭 사용 원칙
- citation_policy: 출처 표기 규칙
- examples: 내 톤으로 쓴 단락 샘플 2개

JSON으로만 출력해라.
글 샘플:
{samples}
"""

def load_samples():
    texts=[]
    for p in glob.glob("data/posts/*"):
        with open(p, encoding="utf-8", errors="ignore") as f:
            t=f.read().strip()
            if t: texts.append(t[:8000])
    return "\n\n---\n\n".join(texts[:5])

def run():
    llm = ChatOpenAI(model="gpt-4o", temperature=0.2)
    prompt = PromptTemplate.from_template(STYLE_EXTRACT_TMPL)
    style_json = llm.invoke(prompt.format(samples=load_samples())).content
    os.makedirs("out", exist_ok=True)
    with open("out/style_profile.json","w",encoding="utf-8") as f:
        f.write(style_json)
    print("[OK] out/style_profile.json 생성")

if __name__=="__main__":
    run()

GPT모델은 4o를 쓰고자 한다. 생각보다 토큰 소모에 따른 비용이 많이 들지는 않는다.

python style_extractor.py

그리고 위 스크립트를 실행시킨다.

이러면 나의 문체를 학습한 규칙이[style_profile.json]과 같이 JSON[1] 형식으로 통합된다.


파싱 실패 오류 방지

하지만 위에서 만든 JSON파일을 그대로 쓸 수는 없다.

왜냐하면 텍스트 내에 \*, \(, \) 등 마크다운 잔재가 뒤섞이면 이는 유효하지 않은 문자이기 때문에 JSON 파서가 읽지 못한다.

그렇다고 이걸 일일이 직접 지울 수는 없는 노릇이니, 보완을 해주는 스크립트를 추가로 작성해야 한다.

notepad repair_style_json.py

위 스크립트 실행 후 메모장을 열고 repair_style_json.py를 구축한다.

# repair_style_json.py — 코드펜스/잡설 제거 + 잘못된 백슬래시 이스케이프 보정
from pathlib import Path
import re, json

p = Path("out/style_profile.json")
if not p.exists():
    raise SystemExit("❌ out/style_profile.json 파일이 없습니다. style_extractor.py를 먼저 실행하세요.")

raw = p.read_text(encoding="utf-8", errors="ignore")

# 1) BOM/공백/코드펜스 제거
raw = raw.lstrip("\ufeff").strip()
raw = re.sub(r"^```(?:json)?\s*", "", raw, flags=re.IGNORECASE)
raw = re.sub(r"\s*```$", "", raw)

# 2) 흔한 제어문자 제거
raw = raw.replace("\r", "").replace("\x00", "").replace("\u200b", "")

# 3) { ... } 또는 [ ... ] 블록만 추출
start, end = raw.find("{"), raw.rfind("}")
if start == -1 or end == -1 or end <= start:
    a_start, a_end = raw.find("["), raw.rfind("]")
    if a_start != -1 and a_end != -1 and a_end > a_start:
        candidate = raw[a_start:a_end+1]
    else:
        print("원본 미리보기(앞 200자):", repr(raw[:200]))
        raise SystemExit("❌ JSON 블록을 찾지 못했습니다. 파일을 수동으로 확인하세요.")
else:
    candidate = raw[start:end+1]

def fix_json_string(s: str) -> str:
    # 스마트 따옴표 → 일반 따옴표
    s = s.replace("“", "\"").replace("”", "\"").replace("‘", "'").replace("’", "'")
    # 잘못된 줄끝 콤마 제거
    s = re.sub(r",\s*([}\]])", r"\1", s)
    # ⚠️ 유효하지 않은 백슬래시 이스케이프(\* \() 등을 \\로 보정
    # JSON 표준 이스케이프: " \ / b f n r t u 만 허용
    s = re.sub(r'\\(?!["\\/bfnrtu])', r'\\\\', s)
    return s

# 4) 1차 파싱 시도
try:
    obj = json.loads(candidate)
except json.JSONDecodeError:
    fixed = fix_json_string(candidate)
    try:
        obj = json.loads(fixed)
        candidate = fixed
    except json.JSONDecodeError as e2:
        print("파싱 실패 미리보기(앞 400자):", candidate[:400])
        raise SystemExit(f"❌ JSON 파싱 실패: {e2}")

# 5) 예상 키 존재 여부 안내(느슨한 검증)
if not isinstance(obj, dict) or not any(k in obj for k in ["voice","do_list","dont_list","phrasing","formatting"]):
    print("⚠️ 경고: 예상 키가 충분치 않을 수 있습니다. 그래도 저장은 계속합니다.")

# 6) 깨끗한 JSON으로 저장
p.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8")
print("✅ 스타일 JSON 복구 완료:", str(p.resolve()))

이러면 파싱 실패 오류가 발생하지 않는다.


vectorstore.py 구축

notepad build_vectorstore.py

다음은 위 스크립트를 실행하여 메모장을 켜고 vectorstore.py를 구축한다.

# build_vectorstore.py
import os
from pathlib import Path
from dotenv import load_dotenv

from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema import Document

def main():
    load_dotenv()  # .env에서 OPENAI_API_KEY 읽기
    api_key = os.getenv("OPENAI_API_KEY")
    if not api_key:
        raise RuntimeError("OPENAI_API_KEY가 설정되지 않았습니다. 프로젝트 루트의 .env를 확인하세요.")

    # ✅ 문체 소스 폴더
    src_dir = Path("data") / "posts"
    if not src_dir.exists():
        raise FileNotFoundError(f"소스 폴더가 없습니다: {src_dir.resolve()}")

    # .txt / .md / .markdown 파일만 수집
    exts = {".txt", ".md", ".markdown"}
    files = [p for p in src_dir.rglob("*") if p.suffix.lower() in exts]
    if not files:
        raise FileNotFoundError(f"{src_dir.resolve()} 안에 .txt / .md / .markdown 파일이 없습니다.")

    # 문서 로딩
    docs = []
    for p in files:
        try:
            text = p.read_text(encoding="utf-8", errors="ignore").strip()
            if text:
                docs.append(Document(page_content=text, metadata={"source": str(p.relative_to(src_dir))}))
        except Exception as e:
            print(f"[경고] 파일을 건너뜁니다: {p} ({e})")

    if not docs:
        raise RuntimeError("읽어들인 문서가 없습니다. 파일 인코딩/내용을 확인하세요.")

    # 청크 분할
    splitter = RecursiveCharacterTextSplitter(chunk_size=900, chunk_overlap=150)
    chunks = splitter.split_documents(docs)
    print(f"문서 {len(docs)}개 → 청크 {len(chunks)}개")

    # 임베딩 & 벡터스토어
    embeddings = OpenAIEmbeddings()
    vs = FAISS.from_documents(chunks, embeddings)

    # 저장 경로: out/faiss_posts
    out_dir = Path("out")
    out_dir.mkdir(exist_ok=True)
    save_path = out_dir / "faiss_posts"
    vs.save_local(str(save_path))
    print(f"✅ 벡터스토어 저장 완료: {save_path.resolve()}")

if __name__ == "__main__":
    main()

완료되면 아래 코드를 실행한다.

python build_vectorstore.py
  • vectorstore: 텍스트를 벡터로 변환해 저장하고 유사도 검색을 수행하는 데이터베이스 (RAG[2] 구현)

이 과정을 통해 AI가 나의 문체가 담긴 샘플 글을 더 작은 단위로 쪼개고(Chunking) → 임베딩(벡터화)하여(tiktoken 활용) → FAISS(faiss-cpu 활용)인덱스로 저장한다.

이제 다음 요청부터는 AI가 해당 인덱스를 서칭하여 나의 말투를 참고할 것이다.


main.py 구축

이제 main.py를 구축하겠다.

notepad main.py

다시 한번 메모장을 오픈해 주고 아래 장문의 코드를 붙여 넣는다.

# main.py
import argparse, os, re, json, datetime, pathlib, time
from dotenv import load_dotenv
import requests
from bs4 import BeautifulSoup
from tavily import TavilyClient
import time

from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.prompts import PromptTemplate
from langchain.schema import StrOutputParser
from tavily import TavilyClient

# ---------- 유틸 ----------
def sanitize_slug(s: str) -> str:
    s = s.lower()
    s = re.sub(r"[^a-z0-9\- ]+", "", s)
    s = re.sub(r"\s+", "-", s).strip("-")
    return s[:80] if len(s)>80 else s

def to_yaml_front_matter(title: str, slug: str, summary: str, tags=None) -> str:
    if tags is None:
        tags = ["AI"]
    tags_yaml = "[" + ", ".join(f'"{t}"' for t in tags) + "]"
    date_str = datetime.date.today().isoformat()
    return (
        "---\n"
        f'title: "{title}"\n'
        f'slug: "{slug}"\n'
        f'date: "{date_str}"\n'
        f"tags: {tags_yaml}\n"
        f'summary: "{summary}"\n'
        "---\n\n"
    )

def load_vectorstore(path="out/faiss_posts"):
    return FAISS.load_local(path, OpenAIEmbeddings(), allow_dangerous_deserialization=True)

def load_style_json(path="out/style_profile.json") -> dict:
    with open(path, encoding="utf-8") as f:
        return json.load(f)

# ---------- 웹검색 ----------
def tavily_sources(query: str, k: int = 5):
    """Tavily SDK로 상위 k개 결과(title, url, snippet) 가져오기"""
    try:
        client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
        resp = client.search(query=query, max_results=k)
        out = []
        for h in resp.get("results", []):
            url = h.get("url", "")
            title = h.get("title", "")
            snippet = h.get("content", "") or h.get("snippet", "")
            if url.startswith("http://") or url.startswith("https://"):
                out.append({"title": title, "url": url, "snippet": snippet})
        return out
    except Exception as e:
        print(f"[경고] Tavily 검색 실패: {e}")
        return []

def fetch_quote(url: str, limit: int = 700):
    try:
        r = requests.get(url, timeout=15, headers={"User-Agent":"Mozilla/5.0"})
        soup = BeautifulSoup(r.text, "html.parser")
        texts = [t.get_text(" ", strip=True) for t in soup.select("p, li")]
        txt = " ".join(texts).strip()
        return (txt[:limit] + "...") if len(txt) > limit else txt
    except Exception:
        return ""

def build_bibliography(topic: str, k: int = 5):
    """다중 검색 결과에 간단 인용문을 붙여 참고자료 리스트 생성"""
    hits = tavily_sources_multi(topic, k=k)
    bib = []
    for hit in hits:
        quote = fetch_quote(hit["url"])
        bib.append({
            "id": len(bib) + 1,
            "title": hit.get("title") or hit["url"],
            "url": hit["url"],
            "quote": quote or hit.get("snippet", "")
        })
        time.sleep(0.2)
    return bib

def expand_queries(topic: str):
    """주제를 다양한 각도로 쪼갠 하위 쿼리들"""
    return [
        f"{topic} 정의와 기원",
        f"{topic} 주요 특징과 사용례",
        f"{topic} 관련 논란/오해",
        f"{topic} 데이터/통계/사례",
        f"{topic} 공식 문서/위키/백서",
        f"{topic} 최신 동향 2024..2025",
    ]

def tavily_sources_multi(topic: str, k: int = 5):
    """여러 하위 쿼리로 Tavily를 호출 → URL 기준 dedupe 후 리스트 반환"""
    client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
    bag = {}  # url → {title,url,snippet}
    for q in expand_queries(topic):
        try:
            resp = client.search(
                query=q,
                max_results=k,
                search_depth="advanced",
                include_answer=False,
            )
            for h in resp.get("results", []):
                url = h.get("url", "")
                if url.startswith("http"):
                    bag[url] = {
                        "title": (h.get("title") or url),
                        "url": url,
                        "snippet": (h.get("content", "") or h.get("snippet", "")),
                    }
        except Exception as e:
            print(f"[경고] Tavily 검색 실패({q}): {e}")
        time.sleep(0.25)
    return list(bag.values())

# ---------- 프롬프트 (★ 문체 규약 주입) ----------
DRAFT_TMPL = """너는 아래의 **문체 규약**을 반드시 준수하는 한국어 기술 블로거다.
규약은 절대 변경하거나 무시하지 마라.

[문체 규약(JSON)]
{style_json}

[주제] {topic}
[독자] {audience}
[톤]   {voice}
[언어] {lang}

[내 과거 글에서 추출한 컨텍스트(RAG)]
{context}

[외부 출처(요약 + 링크)]
{bib_json}

지시사항(규약을 구체화):
1) 마크다운으로 작성: H2 1개 → H3 중심. 짧은 문장/단락, 과장 금지.
2) 시니컬 유머는 규약.humor 빈도/스타일을 따를 것. 유머 때문에 정보 정확도를 해치지 말 것.
3) 단정적 주장에는 각주 표기[^n] 필수. 문서 끝 '## 참고자료' 섹션에 [^n]: 제목 — URL — 1~2문장 인용.
4) 모호하거나 출처가 불분명하면 **추정/가설**로 명시.
5) YAML 프론트매터는 출력하지 말고, 본문만 출력.
6) 같은 문장/문구를 반복하지 말고, 대신 새로운 사실, 수치, 사례를 제시할 것.

출력: H2 제목부터 시작한 Markdown 본문만.
"""

# ---------- 메인 ----------
def main():
    load_dotenv()
    if not os.getenv("OPENAI_API_KEY"):
        raise RuntimeError("OPENAI_API_KEY가 .env에서 읽히지 않습니다.")
    if not os.getenv("TAVILY_API_KEY"):
        print("[안내] TAVILY_API_KEY 없음: 웹검색 없이 진행합니다.")

    parser = argparse.ArgumentParser()
    parser.add_argument("--topic", required=True)
    parser.add_argument("--lang", default="ko")
    parser.add_argument("--audience", default="AI에 관심있는 누구나")
    parser.add_argument("--voice", default="간결하고 실용적인 한국어 설명, 약간의 시니컬 유머")
    parser.add_argument("--model", default="gpt-4o")
    parser.add_argument("--k", type=int, default=6, help="RAG 컨텍스트 청크 수")
    parser.add_argument("--webk", type=int, default=6, help="웹검색 결과 수")
    parser.add_argument("--style_strength", type=float, default=1.0, help="0.0~1.0, 1.0=규약 그대로")
    args = parser.parse_args()

    # 1) 문체 규약 로드
    try:
        style = load_style_json("out/style_profile.json")
    except Exception as e:
        raise RuntimeError("style_profile.json이 없습니다. 먼저 style_extractor.py를 실행해 문체 프로파일을 만들어주세요.\n" + str(e))

    # style_strength로 유머 강도 등 보정(원하면 사용)
    if 0 <= args.style_strength < 1.0 and "humor" in style:
        style["humor"]["frequency"] = "low" if args.style_strength < 0.5 else "med"

    # 2) RAG 컨텍스트
    context_text = "(컨텍스트 없음)"
    try:
        vs = load_vectorstore("out/faiss_posts")
        ctx_docs = vs.max_marginal_relevance_search(args.topic, k=args.k, fetch_k=max(20, args.k*4), lambda_mult=0.5)
        context_text = "\n\n---\n\n".join(d.page_content[:1200] for d in ctx_docs) if ctx_docs else "(컨텍스트 없음)"
    except Exception as e:
        print(f"[안내] 벡터스토어 로드 생략: {e}")

    # 3) 웹검색 참고자료
    bibliography = build_bibliography(args.topic, k=args.webk) if os.getenv("TAVILY_API_KEY") else []
    if not bibliography:
        bibliography = [{"id": 1, "title": "자료 부족", "url": "", "quote": "공신력 있는 자료 부족"}]

    # 4) 초안 생성 (★ temperature 낮추기)
    llm = ChatOpenAI(model=args.model, temperature=0.15, frequency_penalty=0., presence_penalty=0.3, max_tokens=1800)
    prompt = PromptTemplate.from_template(DRAFT_TMPL)
    chain = prompt | llm | StrOutputParser()

    draft = chain.invoke({
        "style_json": json.dumps(style, ensure_ascii=False, indent=2),
        "topic": args.topic,
        "audience": args.audience,
        "voice": args.voice,
        "lang": args.lang,
        "context": context_text,
        "bib_json": json.dumps(bibliography, ensure_ascii=False, indent=2),
    })

    # 5) 저장
    title = args.topic
    slug = sanitize_slug(title)
    fm = to_yaml_front_matter(title=title, slug=slug, summary=args.topic)
    out_dir = pathlib.Path("out"); out_dir.mkdir(exist_ok=True)
    out_path = out_dir / f"{datetime.date.today().isoformat()}-{slug}.md"
    with open(out_path, "w", encoding="utf-8") as f:
        f.write(fm); f.write(f"# {title}\n\n"); f.write(draft.strip() + "\n")
    print(f"[OK] 파일 저장: {out_path.resolve()}")

if __name__ == "__main__":
    main()

여기서 #4 초안 생성 파트에 쓰인★ temperature는 일종의 Creativity level과 비슷한 개념으로, 일관성을 유지하기 위해서는 가능한 낮추는 것이 좋지만, 또 너무 낮추면 같은 단어를 반복하는 등의 문제가 생길 수 있기 때문에 적당한 선에 조절이 필요하다.

 

드디어 모든 세팅 과정이 끝났다.


주제 제공 : 나노 바나나

이제 세팅은 완료되었으니, AI에게 포스팅 주제를 제공해 보자.

최근 갑자기 LMArena에 등장한 '나노 바나나'에 대해 알아보라고 시켜보겠다. ('출시'가 아니라 '등장'이다.)

 

참고로 LMArena는 버클리 대학 연구진이 설립한 영리 단체이다.

*우리가 흔히 알고있는 보스턴에 위치한 '버클리 음대'의 'Berklee'가 아니고 캘리포니아 공과대학인 'Berkeley'이다.

여기에 TMI를 좀 더하자면, 버클리 음대는 원래는 “Schillinger House”라는 이름이었는데, 1954년에 창립자 Lawrence Berk가 자신의 성씨(Berk)와 아들의 이름(Lee)을 따서 Berklee로 개명했고, 공과대학 버클리는 애초에 캘리포니아주 버클시에 위치하며, 도시 이름인 버클리는 18세기 철학자 George Berkeley의 이름에서 따왔다고 한다.

Nano-Banana: The New Image Generative AI Making Waves by LMArena AI @Flux AI

Nano Banana는 이전 5-1주 차 포스팅에서 다뤘던 Avenger 0.5와 거의 똑같은 패턴을 띄고 있는 놈이다.

 

[week5-1] Avenger 0.5 넌 정체가 뭐니? feat.Artificial Analysis

이번 5주 차 포스팅에서는 Artificial Analysis Video Arena Leaderboard에 아무런 소리소문 없이 곧장 '3위'로 데뷔한 Avenger 0.5에 대해 이야기해보려 한다. *현재는 4위로 내려갔다.(작성일 기준)하지만 그전

mapsycoy.tistory.com

둘의 공통점은 비공개 모델이지만 배틀모드에서 간접적으로 사용할 수 있다는 것이다.

하지만 가장 큰 차이가 있는데, 프롬프팅이 외부에 반영구적으로 노출되는 Artificialanalysis Video Arena와는 달리, LMArena 배틀모드는 나의 프롬프팅이 외부에 공개되지 않고, 원활하게 다운로드도 가능하다는 것이다.

그러나 결국에는 둘 모두 elo 매칭 시스템으로 매번 여러 모델들이 랜덤 등장하기 때문에 사용이 제한적이고, 그만큼 운이 또 따라야 한다.

그럼에도 내 주변 팀원들은 모두 5트 내에 사용을 해보았고, 여러 커뮤니티를 찾아봐도 대부분 10트 내에는 뜨는 것 같던데.. 나는 연속 39번을 도전했지만 단 한 번도 등장하지 않았다.😇 (괜히 40번 채웠다가 안 되면 화병 날까 봐 관뒀다.)

이미지 퀄리티가 매우 뛰어나 꼭 써보고 싶었지만 그러지 못하고 있는 상황이다.

그리고 그 성능에 걸맞게 등장 직후 얼마나 빨리 입소문을 탔는지 벌써부터 해당 모델의 이름을 마치 공식 사이트인 것처럼 갖다 쓰는 스캠 웹사이트도 있다.

스캠 사이트

나노 바나나는 아직 비공개 모델이기 때문에 LMArena 외에 이를 서비스한다는 웹사이트가 존재한다면 그건 100% 사기이니 주의하자. + 구글에서 개발했다는 얘기가 돌고 있던데, 그건 어디까지나 루머일 뿐 그 무엇도 밝혀진 것은 없다.

 

아무튼 상황이 이렇다 보니 이 주제에 한해서는 gpt에게 직접 블로그 글을 쓰게 하더라도 당장은 사실관계를 입증할만한 것이 거의 없어 테스트용으로 괜찮을 것 같다.

 

자, 이제 정말 마지막 단계만 남았다.

cmd 창에 아래와 같이 입력하고 스크립트를 실행하자.

python main.py --topic "Nano Banana, 넌 정체가 뭐니?" --lang ko

그러면 AI는 잠시 생각 후 [out/2025-08-20-nano-banana.md]를 생성해 준다.

 

이제 이것을 그대로 티스토리의 마크다운 모드에 붙여 넣고 약간의 텍스트 사이즈 등을 손봐주면 끝이다.

*다만 이미지 파일은 내가 직접 서버에 올려야 한다는 것이 단점

 

###하단의 글은 나의 문체를 학습한 gpt가 작성한 글이다. 구분을 위해 초록색으로 표시하겠다.###

그냥 재미로만 봐달라.


By. 나의 문체를 학습한 GPT-4o : Nano Banana, 넌 정체가 뭐니?

title: "Nano Banana, 넌 정체가 뭐니?"
slug: "nano-banana"
date: "2025-08-20"
tags: ["AI"]
summary: "Nano Banana, 넌 정체가 뭐니?"

Nano Banana의 등장

대체 이건 뭐지? 'Nano Banana'라는 이름을 들었을 때, 바나나의 새로운 품종인가 싶겠지만, 사실 이건 구글이 개발한 것으로 추정되는 이미지 생성 AI 모델이다. 이 AI는 포토샵을 대체할 수 있을 만큼 강력하다는 소문이 돌고 있다[^1]. 그렇다면 이게 가능한 일이냐!

Nano Banana의 기능과 특징

Nano Banana는 이미지 생성과 편집에서 일관성을 유지하는 데 탁월한 성능을 보인다. 예를 들어, 동일한 인물의 얼굴을 다양한 감정과 배경으로 변환할 수 있다. 이게 가능한 일이냐! 라고 묻고 싶겠지만, 실제로 가능하다니 놀라울 따름이다[^2].

구글과의 연관성

구글이 과일 이름을 코드네임으로 사용하는 전례가 있어, Nano Banana가 구글의 비밀 프로젝트일 가능성이 높다. 물론, 공식 발표는 없으니 이건 어디까지나 추정이다[^3]. 그래도 구글이 이런 걸 만들었다면, 포토샵은 이제 긴장해야 할지도 모른다.

활용 사례

Nano Banana는 다양한 분야에서 활용될 수 있다. 예를 들어, 마케팅에서 여러 장의 이미지를 일관되게 생성하거나, 영상 제작에서 캐릭터의 일관성을 유지하는 데 유용하다. 결론적으로 말하자면, 이 AI는 창작의 새로운 지평을 열고 있다[^4].

결론

결론적으로 말하자면, Nano Banana는 아직 그 정체가 명확히 밝혀지지 않았지만, 그 성능만큼은 이미 많은 이들의 관심을 끌고 있다. 어쨌든, 이 AI가 앞으로 어떤 변화를 가져올지 기대된다.

참고자료

[^1]: 포토샵 뛰어넘는 AI? '트럼프 신라면' 뚝딱 만든 나노 바나나 — https://v.daum.net/v/20250819130623231 — "사진 속 손흥민 선수의 피겨를 만들어줘. 전시용 스탠드에 올리고, 옆에는 포장 박스도 함께 만들어줘."
[^2]: The SECRET Photoshop Killer (NEW Insane AI Editing + Character Consistency) — https://www.youtube.com/watch?v=E-BenZQSGog — "A mysterious new model called “Nano Banana” just appeared on LMArena (people think it is Google's new image model) and its text-based editing and character consistency are wild."
[^3]: 포토샵 대체한다고? 정체불명 AI에 쏠린 관심… '나노 바나나' 뭐길래 — https://biz.chosun.com/it-science/ict/2025/08/18/KRYU57ACIRGJLAGA3I6AMARWW4/ — "나노 바나나의 정체는 아직 불분명하다. 다만 구글이 내부 프로젝트에 과일 이름을 코드네임으로 써온 전례 때문에, “차세대 구글 이미지 모델일 가능성”이라는 추측이 힘을 얻고 있다."
[^4]: 현존하는 유일한 '나노바나나 ai' 사용법 알려드립니다! — https://www.youtube.com/watch?v=pT_JbjsCK9o — "나노 바나나 압도적이네 ㅋㅋㅋ 계속 안 나오길래 같은 프롬포트 돌리다가 혼자 압도적 퀄리티로 뽑아주길래 아 너구나 했더니 나노바나나 ㅋㅋㅋ."


오늘의 결론

"결론적으로 말하자면22", AI가 쓴 글은 별로이다.

 

압도적인 성능의 미스테리 AI 모델 ‘나노 바나나’

차세대 이미지 생성 모델 ‘나노 바나나(Nano Banana)’를 둘러싼 관심이 뜨겁습니다. 최근 레딧의 AI 모델 평가 플랫폼 LMArena에 이 이름이 등장하면서 정체불명의 모델에 대한 추측이 이어지고 있

designcompass.org

⬆️근데 정말 놀랍게도 gpt가 자동으로 작성해준 글은 위 링크 속 게시물 내용 및 흐름과 매우 유사하다.

이곳에서 정보를 긁어온 것인지, 아니면 위 게시물을 작성한 사람이 gpt가 써준 글을 그대로 붙붙한 것인지는 알 수 없다만~

다소 오해를 불러일으킬 수 있는 문구가 있어, 이 부분은 정정하고자 한다. 

바로 이 문장이다. "구글이 과일 이름을 코드네임으로 사용하는 전례"

 

⬇️아래는 위키피디아 등 출처를 기반으로 작성한 구글의 안드로이드, 픽셀 버전별 코드네임 리스트이다.

 안드로이드 버전 코드네임 픽셀 버전 코드네임
Android 1.1 Petit Four Pixel / Pixel XL Sailfish / Marlin
Android Cupcake Cupcake Pixel 2 / Pixel 2 XL Walleye / Taimen
Android Donut Donut Pixel 3 / Pixel 3 XL Blueline / Crosshatch
Android Eclair Eclair Pixel 3a / Pixel 3a XL Sargo / Bonito
Android Froyo Froyo Pixel 4 / Pixel 4 XL Flame / Coral
Android Gingerbread Gingerbread Pixel 4a / Pixel 4a 5G Sunfish / Bramble
Android Honeycomb Honeycomb Pixel 5 / Pixel 5a Redfin / Barbet
Android Ice Cream Sandwich Ice Cream Sandwich Pixel 6 / Pixel 6 Pro Oriole / Raven
Android Jelly Bean Jelly Bean Pixel 6a Bluejay
Android KitKat Key Lime Pie Pixel 7 / Pixel 7 Pro Panther / Cheetah
Android Lollipop Lemon Meringue Pie Pixel 7a Lynx
Android Marshmallow Macadamia Nut Cookie Pixel 8 / Pixel 8 Pro Shiba / Husky
Android Nougat New York Cheesecake Pixel Fold Felix
Android Oreo Oatmeal Cookie Pixel 8a Akita
Android Pie Pistachio Ice Cream Pixel 9 / Pixel 9 Pro / Pixel 9 Pro XL Tokay / Caiman / Komodo
Android 10 Quince Tart Pixel 9a Tegu
Android 11 Red Velvet Cake Pixel 10 / Pixel 10 Pro / Pixel 10 Pro XL Frankel / Blazer / Mustang
Android 12 Snow Cone    
Android 12L Snow Cone v2    
Android 13 Tiramisu    
Android 14 Upside Down Cake    
Android 15 Vanilla Ice Cream    
Android 16 Baklava    

표를 보면 알 수 있듯, 순수 과일 이름을 사용한 전례는 없고 죄다 디저트, 동물 이름이다.

이외 클라우드 서비스에서는 주로 기술명칭을 사용하는 것으로 확인된다.

이래서 번거롭더라도 AI가 제공하는 정보는 항상 2차 검증을 꼭 거치는 것이 좋다.


+ 내용 추가(08.27)

나노바나나는 구글이 개발한 것이 맞았다.

그리고 이전 5주 차에서 다뤘던 이와 비슷한 스타일의 비공개 비디오 생성 모델, Avenger 0.5는 소리소문 없이 사라져버렸다.

 

[week5-1] Avenger 0.5 넌 정체가 뭐니? feat.Artificial Analysis

이번 5주 차 포스팅에서는 Artificial Analysis Video Arena Leaderboard에 아무런 소리소문 없이 곧장 '3위'로 데뷔한 Avenger 0.5에 대해 이야기해보려 한다. *현재는 4위로 내려갔다.(작성일 기준)하지만 그전

mapsycoy.tistory.com

 

  1. JavaScript Object Notation의 약자로, 아주 경량화된 데이터의 값을 정의하는 문서라고 보면 된다. (예)2D 리깅 작업에 주로 사용되는 Spine pro에서 bone과 키프레임 등의 데이터가 json 형식으로 파일이 저장되며, 이걸 그대로 Unity에 던져 넣을 수가 있다.
  2. *RAG(Retrieval-Augmented Generation)는 외부 데이터 소스에서 정보를 검색해 LLM의 응답을 보강하는 것을 뜻한다. 이에 대해서는 지난 5-2주 차 포이즈닝 파트에서 잠시 다뤘었다.