HyperAI초신경
Back to Headlines

AST 활용으로 웹앱 코드 이미지 URL 자동 교체 성공

18일 전

최근에 우리는 사용자가 간단한 프롬프트로 웹 앱을 구축할 수 있는 플랫폼을 개발하고 있었다. 이 플랫폼은 v0와 loveable처럼 작동하도록 설계되었으며, 당연히 대부분의 기술 스타트업처럼 대형 언어 모델(LLM)을 활용하여 소스 코드를 생성했다. 그러나 이 접근 방식이 다양한 문제를 초래했지만, 특히 프론트엔드 코드에서 환영된 이미지 URL 문제가 독특했다. 웹 페이지의 소스 코드에는 거의 항상 몇 개의 이미지 요소가 포함되며, 이 코드가 LLM으로 생성되면 이미지 URL이 유효하지 않은 경우가 많다. 따라서 어떻게 하면 생성된 코드가 유효하고 관련성 있는 이미지 URL을 포함하도록 할 수 있을까? 이 문제를 더 작은 단위로 나누면 다음과 같이 표현할 수 있다: 1. 코드 내에서 이미지에 해당하는 모든 부분을 찾는다. 2. 이미지 발생 부분 주변의 컨텍스트와 추가 정보를 사용하여 각 이미지에 대한 적절한 캡션을 생성한다. 3. 텍스트-이미지 모델을 사용하여 캡션에 기반한 이미지를 생성한다. 4. 환영된 이미지 URL을 생성된 이미지 URL로 대체한다. 이 글에서는 주로 1번과 4번에 대해 집중할 것이다. 2번과 3번은 비교적 간단한 LLM 호출로 이루어져 있기 때문이다. 옵션 1: 정규표현식(Regex) 이미지 URL을 찾는 것은 정규표현식을 사용하면 간단해 보일 수 있다. 예를 들어, JSX에서 <img> 태그나 프레임워크 특유의 <Image> 태그 등으로 이미지가 표현될 수 있다. ```jsx import Image from 'next/image'; const component = () => ( ); ``` 하지만, 여러 이유로 이 방법은 제한적이다: - 이미지는 <ProfileImage> 같은 사용자 정의 컴포넌트로도 표현될 수 있다. - JSX 이미지 태그의 속성이 JavaScript에서 정의된 리스트나 객체를 참조할 수도 있다. ```jsx const imageList = [ { src: "https://cdn.site.com/img1.png" }, { src: "https://cdn.site.com/img2.png" }, ]; const component = () => ( {imageList.map((img) => ( ))} ); ``` 옵션 2: LLM 정규표현식이 도움이 되지 않는다면, LLM에 코드를 전달하여 모든 이미지 발생 부분을 추출하는 방법을 고려할 수 있다. 대부분의 LLM은 이러한 경우를 잡아낼 만큼 똑똑하다. 하지만, 이 방법에도 몇 가지 단점이 있다: - 과도한 처리: LLM을 사용해서 코드의 일부만 추출하는 것은 과도한 처리다. - 비용: LLM 호출 비용은 작지만, 시간이 지남에 따라 누적될 수 있다. - 느림: LLM API 호출은 몇 초가 걸린다. - 비확정적: LLM은 대부분의 경우를 잡아낼 수 있지만, 비확정적인 함수이므로 일부 시나리오에서는 실패할 수 있다. 해결책: 추상 문법 트리(AST) 정규표현식과 LLM 옵션이 적합하지 않다면, AST(Abstract Syntax Tree)를 사용하는 것이 더 합리적이다. AST는 소스 코드를 구문 구성요소로 분해하여 트리 구조로 배열한다. 이를 통해 특정 기준에 맞는 모든 이미지 노드를 찾아낼 수 있다. Babel을 사용하면 JavaScript/TypeScript의 AST를 쉽게 생성할 수 있다. 아래 예제를 통해 이를 확인할 수 있다: ```javascript const babelParser = require('@babel/parser'); const traverse = require('@babel/traverse').default; const generator = require('@babel/generator').default; const t = require('@babel/types'); function findImageNodes(code) { const ast = babelParser.parse(code, { sourceType: 'module', plugins: ['jsx', 'typescript'], }); const candidateNodes = []; traverse(ast, { ObjectExpression(path) { const properties = path.node.properties; properties.forEach((prop) => { if ( prop.type === 'ObjectProperty' && (prop.key.name === 'src' || prop.key.name === 'url' || prop.key.name === 'image') ) { const nodeSourceCode = generator(path.node).code; candidateNodes.push(nodeSourceCode); } }); }, JSXElement(path) { const openingElement = path.node.openingElement; const tagName = openingElement.name && openingElement.name.name; if (tagName === 'img' || tagName === 'Image') { const nodeSourceCode = generator(openingElement).code; const existingSrcAttr = openingElement.attributes.find(attr => attr.name.name === 'src'); if (existingSrcAttr) { if (existingSrcAttr.value.type === 'StringLiteral') { candidateNodes.push(nodeSourceCode); } } else { candidateNodes.push(nodeSourceCode); } } } }); return candidateNodes; } ``` 이 코드는 주어진 소스 코드에서 AST를 생성하고, 이를 순회하여 JSX 요소 또는 JavaScript 객체 표현식 중 우리의 기준에 맞는 모든 이미지 노드를 찾는다. 노드에 대한 참조를 얻은 후 필터링하거나 필요하면 2번과 3번(캡션 및 이미지 생성)을 실행할 수 있다. 그 결과를 각 노드에 다시 삽입하고 AST를 소스 코드로 변환한다. ```javascript function updateImageNodesWithMetaData(code, nodeResults) { const ast = babelParser.parse(code, { sourceType: 'module', plugins: ['jsx', 'typescript'], }); const resultMap = new Map(nodeResults); traverse(ast, { ObjectExpression(path) { const properties = path.node.properties; const nodeSourceCode = generator(path.node).code; if (resultMap.has(nodeSourceCode)) { properties.forEach((prop) => { if ( prop.type === 'ObjectProperty' && (prop.key.name === 'src' || prop.key.name === 'url' || prop.key.name === 'image') ) { prop.value = t.stringLiteral(resultMap.get(nodeSourceCode)['image_url']); const existingAlt = properties.find((p) => p.key.name === 'alt'); if (existingAlt) { existingAlt.value = t.stringLiteral(resultMap.get(nodeSourceCode)['description']); } else { const altProperty = t.objectProperty( t.identifier('alt'), t.stringLiteral(resultMap.get(nodeSourceCode)['description']) ); path.node.properties.push(altProperty); } } }); } }, JSXElement(path) { const openingElement = path.node.openingElement; const nodeSourceCode = generator(openingElement).code; if (resultMap.has(nodeSourceCode)) { const srcAttr = openingElement.attributes.find( (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'src' ); if (srcAttr) { srcAttr.value = t.stringLiteral(resultMap.get(nodeSourceCode)['image_url']); const existingAltAttr = openingElement.attributes.find( (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'alt' ); if (existingAltAttr) { existingAltAttr.value = t.stringLiteral(resultMap.get(nodeSourceCode)['description']); } else { openingElement.attributes.push( t.jsxAttribute( t.jsxIdentifier('alt'), t.stringLiteral(resultMap.get(nodeSourceCode)['description']) ) ); } } } } }); return generator(ast).code; } ``` 예시 이 섹션에서는 다양한 형태(JSX 요소, 객체 표현식)로 표현된 환영된 이미지 URL을 포함하는 예제 소스 코드를 보여주며, 출력에서는 구조적 무결성이나 구문을 유지하면서 생성된 이미지 URL과 캡션으로 대체된다. 입력 jsx export default function EcommerceTShirts() { const [menuOpen, setMenuOpen] = useState(false); const [searchOpen, setSearchOpen] = useState(false); const hats = [ { id: 1, name: "Simple Hat", price: "$24.99", image: "/hat.jpg", alt: "existing" }, { id: 2, name: "New Hat", price: "$19.99", image: "/hat-new.jpg" }, ]; return ( <div className="flex flex-col min-h-screen bg-gray-50"> <main className="flex-grow mt-20"> <section className="py-16"> <div className="container mx-auto px-4"> <h2 className="text-3xl font-bold mb-8 text-center text-gray-800">Featured T-Shirts</h2> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8"> [ { id: 1, name: "Graphic Tee", price: "$24.99", image: "/tshirt1.jpg" }, { id: 2, name: "Pocket Tee", price: "$19.99", image: "/tshirt2.jpg" }, { id: 3, name: "V-Neck Tee", price: "$22.99", image: "/tshirt3.jpg" }, { id: 4, name: "Crew Neck Tee", price: "$21.99", image: "/tshirt4.jpg" }, ].map((shirt) => ( <Card key={shirt.id} className="transition transform hover:scale-105 hover:shadow-lg"> <CardHeader> <Image src={shirt.image} alt={shirt.name} width={300} height={300} className="w-full h-64 object-cover rounded-t-lg" /> </CardHeader> <CardContent> <CardTitle className="text-xl font-semibold">{shirt.name}</CardTitle> <p className="text-gray-600">{shirt.price}</p> </CardContent> <CardFooter> <Button className="w-full bg-indigo-600 text-white hover:bg-indigo-700 transition duration-300"> Add to Cart </Button> </CardFooter> </Card> )) </div> </div> </section> <section className="py-16"> <div> <img src="/placeholder.jpg?height=400&width=1200" alt="A featured hat displayed in a stylish setting, showcasing a vibrant background with a gradient blending from blue to indigo, creating an inviting atmosphere for potential customers." className="w-full h-96 object-cover opacity-50" id="08ba0c93-1f24-4a28-a459-4f115299849f" /> </div> </section> <section className="py-16"> <div> <Image height={900} src="/placeholder.png" alt="A placeholder image for a t-shirt" width={1200} className="w-full h-96 object-cover opacity-50" id="08ba0c93-1f24-4a28-a459-4f115299849f" /> </div> </section> <section className="py-16"> <div> {hats.map((hat) => ( <img key={hat.id} src={hat.image} alt={hat.name} className="w-full h-96 object-cover opacity-50" id={hat.id.toString()} /> ))} </div> </section> </main> </div> ); } 출력 jsx export default function EcommerceTShirts() { const [menuOpen, setMenuOpen] = useState(false); const [searchOpen, setSearchOpen] = useState(false); const hats = [ { id: 1, name: "Simple Hat", price: "$24.99", image: "s3.productx.hat.jpg", alt: "A simple classic hat in a neutral tone displayed on a flat surface" }, { id: 2, name: "New Hat", price: "$19.99", image: "s3.productx.hat-new.jpg", alt: "A modern-style hat with a minimalist logo on the front panel" }, ]; return ( <div className="flex flex-col min-h-screen bg-gray-50"> <main className="flex-grow mt-20"> <section className="py-16"> <div className="container mx-auto px-4"> <h2 className="text-3xl font-bold mb-8 text-center text-gray-800">Featured T-Shirts</h2> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8"> [ { id: 1, name: "Graphic Tee", price: "$24.99", image: "s3.productx.tshirt1.jpg", alt: "A white graphic t-shirt featuring bold abstract art" }, { id: 2, name: "Pocket Tee", price: "$19.99", image: "s3.productx.tshirt2.jpg", alt: "A soft grey t-shirt with a front chest pocket" }, { id: 3, name: "V-Neck Tee", price: "$22.99", image: "s3.productx.tshirt3.jpg", alt: "A navy V-neck t-shirt made from breathable cotton" }, { id: 4, name: "Crew Neck Tee", price: "$21.99", image: "s3.productx.tshirt4.jpg", alt: "A classic black crew neck t-shirt with a tailored fit" }, ].map((shirt) => ( <Card key={shirt.id} className="transition transform hover:scale-105 hover:shadow-lg"> <CardHeader> <Image src={shirt.image} alt={shirt.alt} width={300} height={300} className="w-full h-64 object-cover rounded-t-lg" /> </CardHeader> <CardContent> <CardTitle className="text-xl font-semibold">{shirt.name}</CardTitle> <p className="text-gray-600">{shirt.price}</p> </CardContent> <CardFooter> <Button className="w-full bg-indigo-600 text-white hover:bg-indigo-700 transition duration-300"> Add to Cart </Button> </CardFooter> </Card> )) </div> </div> </section> <section className="py-16"> <div> <img src="s3.productx.placeholder.jpg" alt="A featured hat displayed in a stylish setting with a vibrant background that fades from blue to indigo, enhancing visual appeal" className="w-full h-96 object-cover opacity-50" id="08ba0c93-1f24-4a28-a459-4f115299849f" /> </div> </section> <section className="py-16"> <div> <Image height={900} src="s3.productx.placeholder.png" alt="A placeholder image representing a featured t-shirt on a blank background" width={1200} className="w-full h-96 object-cover opacity-50" id="08ba0c93-1f24-4a28-a459-4f115299849f" /> </div> </section> <section className="py-16"> <div> {hats.map((hat) => ( <img key={hat.id} src={hat.image} alt={hat.alt} className="w-full h-96 object-cover opacity-50" id={hat.id.toString()} /> ))} </div> </section> </main> </div> ); } 추가 사용 사례 AST는 다양한 용도에서 유용하게 활용될 수 있다: 1. Aider의 블로그 글에서 LLM을 위한 코드 리ント 방법 2. GritQL을 사용하여 대규모 코드 유지보수 및 조작에 대한 훌륭한 강연(AST를 내부적으로 사용) 이 이야기는 Generative AI에서 게재되었다. LinkedIn에서 우리와 연결하고 Zeniteq를 팔로우하여 최신 AI 이야기를 빠짐없이 확인해 보세요. 또한 최신 뉴스와 업데이트를 받기 위해 우리의 뉴스레터와 YouTube 채널을 구독하세요. 함께 AI의 미래를 만들어갑시다!

Related Links