jgjgill

TADD(Test AI Driven Development), AI에 대한 통제권 가져오기

5 min read
TADD(Test AI Driven Development) Taking control of AI

AI와 함께 이것저것 시도해보고 여러 실험을 하면서 어떻게 하면 AI를 잘 다룰 수 있을지 고민해보고 있다.

이러한 과정에서 TADD(Test AI Driven Development) 작업 방식을 시도해보고 좋은 경험을 하게 되어 글을 작성하게 되었다.

AS-IS

문제 상황은 다음과 같았다.

기존에는 문장 내 특정 키워드 강조를 위해 react-highlight-words 라는 라이브러리를 통해 빠르게 구현하고자 했다.

문장 내 특정 키워드에 러닝 포인트 및 체크 포인트 강조 표시를 할 수 있다.


<Highlighter
	searchWords={keywords}
	textToHighlight={koreanText}
	highlightClassName={`${highlightVariants[isSelectedHighlight ? "selected" : "default"]} px-1 rounded cursor-pointer hover:bg-yellow-300 transition-colors`}
	highlightTag="mark"
	onClick={(e: React.MouseEvent<HTMLElement>) => {
		const target = e.target as HTMLElement;

		if (!target.textContent) {
			throw new Error("구문 강조 과정에서 에러 발생");
		}

		onLearningPointClick(sentenceOrder, target.textContent);
	}}
/>


그런데 한 문장 내에 2개 이상의 키워드가 존재할 수 있다.

예를 들면 월세가 얼마인지와 보증금은 얼마나 되는지 알고 싶습니다. 문장에서 월세가 얼마보증금 을 강조하고 싶을 수 있다.

이때 react-highlight-words 가 제공해주는 인터페이스에서는 요구 상황을 구현하는게 까다롭게 느껴졌다. searchWords 에서 배열로 다루어지다보니 문장 내 키워드들이 일괄적으로 하이라이트가 처리된다.



상황에 대한 맥락을 제공하기 위해 기존 코드도 올려본다.

코드 자체는 중요하지 않아서 가볍게 느낌만 파악해도 괜찮다.

"use client";

import type { Tables } from "@repo/typescript-config/supabase-types";
import Highlighter from "react-highlight-words";

type LearningPoint = Tables<"learning_points">;

interface KoreanSentenceHighlighterProps {
	sentenceOrder: number;
	koreanText: string;
	learningPoints: LearningPoint[];
	selectedPoints: Set<string>;
	onLearningPointClick: (sentenceOrder: number, text: string) => void;
}

export default function KoreanSentenceHighlighter({
	sentenceOrder,
	koreanText,
	learningPoints,
	selectedPoints,
	onLearningPointClick,
}: KoreanSentenceHighlighterProps) {
	const getLearningPointKeywords = () => {
		return learningPoints
			.map((point) => point.korean_phrase)
			.filter((phrase) => phrase !== null && phrase !== undefined) as string[];
	};

	const getLearningPointInfo = (highlightedText: string) => {
		return learningPoints.find(
			(point) => point.korean_phrase === highlightedText,
		);
	};

	const isSelectedLearningPoint = (text: string) => {
		const pointInfo = getLearningPointInfo(text);
		if (!pointInfo) return false;
		const pointKey = `${sentenceOrder}-${pointInfo.id}`;
		return selectedPoints.has(pointKey);
	};

	const keywords = getLearningPointKeywords();
	const isSelectedHighlight = keywords.some((keyword) =>
		isSelectedLearningPoint(keyword),
	);

	const highlightVariants = {
		default: "bg-yellow-200",
		selected: "bg-orange-200",
	};

	return (
		<div className="text-lg leading-relaxed">
			<Highlighter
				searchWords={keywords}
				textToHighlight={koreanText}
				highlightClassName={`${highlightVariants[isSelectedHighlight ? "selected" : "default"]} px-1 rounded cursor-pointer hover:bg-yellow-300 transition-colors`}
				highlightTag="mark"
				onClick={(e: React.MouseEvent<HTMLElement>) => {
					const target = e.target as HTMLElement;

					if (!target.textContent) {
						throw new Error("구문 강조 과정에서 에러 발생");
					}

					onLearningPointClick(sentenceOrder, target.textContent);
				}}
			/>
		</div>
	);
}

기존 코드의 수정은 불가피하다. 구조상 더 이상 react-highlight-words 을 사용하는 것은 어렵고 직접 텍스트를 파싱하는 것이 필요하다.


이때 AI에게 어쩌구 저쩌구해서 기능 구현할 수 있게 해줘 등등의 프롬프트로 작업을 진행할 수도 있다. 하지만 지금과 같이 기존 코드들이 존재하는 상황에서의 수정 작업은 불안감이 생긴다. 적지 않은 규모의 코드 수정이 일어나는 경우에는 프롬프트로 아무리 맥락을 잘 제공해줘도 이것만으로 AI에게 전적으로 작업을 맡기는게 거부감이 생긴다.

(마치 AI에게만 의존해서 어두운 길을 걷는 느낌? 사이드 이펙트에 대한 두려움?)


어떻게 문제를 접근할 지 고민하다 팀원분이 공유해주신 좋은 글을 본 기억이 떠올라 한 번 시도를 해봤다.

결과는 대만족?! (TADD: AI를 활용한 새로운 TDD 방법론)

TADD 시도해보기

쉽게 말해서 TDD다. 근데 AI를 곁들인?


참고한 글에서 핵심으로 강조한 부분은 테스트를 기준으로 AI에게 코드 생성을 시키는 것이다.

핵심 3단계

  • 인터페이스 정의
  • 테스트 케이스 작성
  • 테스트 코드에 맞는 코드 작성

적용해보기

우선 인터페이스를 정의한다.

AI에게 구현하고자 하는 요구 사항을 전달해서 어떤 함수를 구성할 지 방향을 잡는다.

대략 다음 함수들이 추가되었다.

  • isSelectedLearningPoint
  • findKeywordAtPosition
  • parseTextSegments

export const isSelectedLearningPoint = (
	pointInfo: LearningPoint,
	sentenceOrder: number,
	selectedPoints: Set<string>,
): boolean => {};

...

export const findKeywordAtPosition = (
	text: string,
	position: number,
	keywords: string[],
): string | undefined => {};

...

export const parseTextSegments = (
	text: string,
	sentenceOrder: number,
	learningPoints: LearningPoint[],
	selectedPoints: Set<string>,
): TextSegment[] => {}

다음으로 인터페이스만 구현된 함수들의 테스트 코드를 구성한다.

AI에게 명세에 기반한 테스트 코드를 따도록 요청한다.

작성된 테스트 항목에서 도움이 되는 항목만 다루도록 한다.


describe("isSelectedLearningPoint", () => {
	it("요구사항에 대한 테스트 명세 작성...", () => { ... })
})

...

describe("findKeywordAtPosition", () => {
	it("요구사항에 대한 테스트 명세 작성...", () => { ... })
})

...

describe("parseTextSegments", () => {
	it("요구사항에 대한 테스트 명세 작성...", () => { ... })
})

TADD 실패 테스트

이제 껍데기만 구성된 함수에 AI와 함께 내용을 채우면서 실패한 테스트들을 성공되도록 하면 된다.

마찬가지로 코드 자체는 중요하지 않아서 가볍게 넘어가도 좋다.


/**
 * @description 텍스트 세그먼트 객체를 생성하는 팩토리 함수
 */

export const createTextSegment = (
	text: string,
	isKeyword: boolean,
	learningPoint?: LearningPoint,
	isSelected = false,
): TextSegment => {
	return {
		text,
		isKeyword,
		learningPoint,
		isSelected: isSelected ?? false,
	};
};

/**
 * @description 특정 위치에서 시작하는 키워드를 찾는 함수 (가장 긴 키워드 우선)
 */

export const findKeywordAtPosition = (
	text: string,
	position: number,
	keywords: string[],
): string | undefined => {
	const filteredKeywords = fx(keywords)
		.filter((keyword) => text.substring(position).startsWith(keyword))
		.toArray();

	return filteredKeywords.sort((a, b) => b.length - a.length)[0];
};

/**
 * @description 텍스트를 키워드와 일반 텍스트로 분리하여 세그먼트 배열을 생성하는 함수
 */

export const parseTextSegments = (
	text: string,
	sentenceOrder: number,
	learningPoints: LearningPoint[],
	selectedPoints: Set<string>,
): TextSegment[] => {
	const keywords = getLearningPointKeywords(learningPoints);

	if (keywords.length === 0) {
		return [createTextSegment(text, false)];
	}

	const segments: TextSegment[] = [];
	const processedKeywords = new Set<string>();
	let currentIndex = 0;

	while (currentIndex < text.length) {
		const foundKeyword = findKeywordAtPosition(text, currentIndex, keywords);

		if (foundKeyword && !processedKeywords.has(foundKeyword)) {
			const learningPoint = getLearningPointInfo(foundKeyword, learningPoints);
			const isSelected = isSelectedLearningPoint(
				learningPoint,
				sentenceOrder,
				selectedPoints,
			);

			segments.push(
				createTextSegment(foundKeyword, true, learningPoint, isSelected),
			);
			processedKeywords.add(foundKeyword);
			currentIndex += foundKeyword.length;
		} else {
			const nextChar = text[currentIndex];
			if (nextChar) {
				const lastSegment = segments[segments.length - 1];

				if (lastSegment && !lastSegment.isKeyword) {
					lastSegment.text += nextChar;
				} else {
					segments.push(createTextSegment(nextChar, false));
				}
			}
			currentIndex += 1;
		}
	}

	return segments;
};

TADD 성공 테스트

TADD 관련 작업 커밋은 해당 링크에서 확인할 수 있다.

feat: TADD 방법론을 활용한 Korean Sentence Highlighter 구현

  • react-highlight-words 대신 커스텀 텍스트 하이라이트 기능 구현
  • 한국어 텍스트에서 여러 키워드의 독립적 토글 상태 관리
  • fxts 라이브러리를 활용한 함수형 프로그래밍 패턴 적용
  • 순수 함수 기반 설계로 테스트 가능성 향상
  • TDD 방식으로 17개 테스트 케이스 작성 및 통과
  • 함수별 JSDoc 문서화로 코드 가독성 개선
  • TADD(Test AI Driven Development) 방법론 문서 작성

좋았던 부분들

AI에 대한 통제권, 옭아매기

기존의 경우 AI와 함께 작업하면 AI의 행동범위가 예측이 안되는 경우가 빈번했다. 너무 많이 변경되면서 AI의 속도를 쫓아가지 못하기도 하고 AI가 잘 수정하고 있는지 확신을 가지지 못하고 불안감이 커졌다. 어렴풋이 오히려 생산성이 저하될 수도 있겠다는 생각이 든 부분이기도 하다.

근데 TADD 방식으로 개발을 진행했을 때 AI 작업 범위를 명확하게 제한할 수 있겠다는 느낌을 받았다.

테스트 코드라는 방파제, 안전 장치를 마련해서 AI에 대한 통제권을 갖을 수 있겠다는 생각을 하게 되었다.

테스트 코드의 효과

그리고 자연스럽게 테스트 코드의 도입이 이루어지기 때문에 코드의 안정성, 명세에 대한 문서화, 좋은 코드 작성에도 많은 도움을 받을 수 있게 되었다.

AI를 통해 많은 것을 해낼 수 있고 덕분에 테스트 코드에 대한 허들도 낮아진 상황에서 TADD 접근을 적극적으로 활용해볼 예정이다.

참고 문서

@2023 powered by jgjgill