텍스트 일괄 치환 앱 개발. '도구를 찾는 시대에서 만드는 시대로'를 실감한 AI와의 대화
요약
작가는 AI와의 대화를 통해 여러 파일의 텍스트를 일괄 치환하는 크로스 플랫폼 앱 'SpeedReplace' 개발 과정을 공유했습니다. 이 앱은 데이터 손상 방지 및 안전성을 최우선 목표로 설정하여, 인코딩 오류나 예상치 못한 파일 변경 등의 위험을 최소화합니다. 아키텍처는 React/TypeScript UI와 Tauri를 통해 Rust 기반의 네이티브 파일 조작 기능을 결합한 구조입니다.
핵심 포인트
- AI와의 대화를 활용해 앱 기획부터 완성까지 개발 전 과정을 주도적으로 진행함.
- 데이터 안전성을 최우선으로 고려하여, 치환 과정에서 발생할 수 있는 인코딩 오류 등을 방지하는 데 초점을 맞춤.
- React/TypeScript와 Tauri를 사용하여 크로스 플랫폼 환경을 구축하고 네이티브 파일 조작은 Rust에 맡김.
- Tauri 사용의 장점으로 Electron보다 작은 구조로 Windows, macOS, Linux 지원이 가능함을 강조함.
여러 파일의 텍스트를 일괄적으로 치환할 수 있는 앱을, 오랫동안 사용해 온 speeeeed를 현대적인 감각으로 개선하고 싶다는 생각에서 시작했습니다. AI에게 처음 내린 지시는 세 가지였습니다. 사용한 모델은 GPT-5.6 Sol입니다.
가장 먼저 ChatGPT의 채팅 모드에서 AI에게 던진 것이 이것이었습니다.
먼저 이 페이지를 이해해 줘
받은 답변은 다음과 같았습니다.
지정된 페이지를 확인하고 내용을 이해했습니다.
speeeeed: 여러 파일의 문자열을 일괄 치환하는 무료 소프트웨어
1. speeeeed란?
...
다음으로 던진 것은 이것이었습니다.
이 앱은 매우 오래되었지만, 이와 동등하거나 더 나은 기능을 가진 현대에 적합한 앱을 크로스 플랫폼으로 만들고 싶어
받은 답변은 다음과 같았습니다.
가능합니다. 게다가 speeeeed의 단순한 조작성을 유지하면서, Windows・macOS・Linux에서 작동하는 빠르고 안전한 일괄 문자 치환 앱으로 개발할 가치가 충분합니다.
•
•
...
다음은 이것이었습니다.
Q2=C, Q3=B
로 변경해 줘
이후에 앱 이름을 정하고 Codex로 이동하여 이것을 던졌습니다.
speeeeed 대체 앱 개발
이 스레드 전체를 이해해 줘
그리고 이것을 던진 후 잠들었습니다.
진행해 줘
다음 날 아침, 이들을 던지고 완성되었습니다.
GitHub에 private repository로 푸시해 줘
구현을 시작해 줘
v1.0 완성까지 완료시키고 테스트도 부탁해
최종적으로 README 조정과 다국어화, 공개 전 최종 점검을 AI에게 맡겼고, 여기부터 아래 내용까지 생성해 주었습니다. 수면 시간이나 식사 시간을 제외하면 순수하게 1시간이 걸리지 않았다고 생각합니다.
정말 '도구를 찾는 시대에서 만드는 시대로'를 실감하는 경험이었습니다.
여러 파일의 문자열을 일괄 치환하고 싶다.
원하는 작업 자체는 간단하지만, 실제 현장에서는 다음과 같은 걱정이 있습니다.
- Shift_JIS 파일을 UTF-8로 덮어쓰지 않을까?
- BOM이나 CRLF/LF가 저절로 바뀌지는 않을까?
- 정규 표현식(Regular Expression) 실수로 예상치 못한 파일까지 변경하지 않을까?
- 미리보기 후에 다른 프로세스가 업데이트한 파일을 덮어쓰지 않을까?
- 뭔가 문제가 생겼을 때 되돌릴 수 있을까?
오랫동안 사용해 온 speeeeed와 같은 간편함은 유지하고 싶습니다. 한편, 지금 새로 만든다면, 치환에 성공하는 것뿐만 아니라 데이터를 손상시키지 않는 것을 최우선으로 하고 싶습니다.
그래서 만든 것이 크로스 플랫폼 일괄 텍스트 치환 앱 SpeedReplace입니다.
- GitHub: StayHomeLabNet/SpeedReplace
- Release: SpeedReplace Releases
구현 전에, 제품으로서의 판단 기준을 정했습니다.
- 단발성 치환에서도 망설임 없이 사용할 수 있다.
- 대규모 폴더에서도 안전하게 사용할 수 있다.
- 미리보기를 생략하고 싶지 않을 속도이다.
- 계정이나 네트워크가 필요하지 않다.
이러한 원칙 덕분에, '속도와 안전성 중 무엇을 우선할까'라는 고민에 빠졌을 때, 안전성을 선택할 수 있었습니다.
구성은 다음과 같습니다.
React + TypeScript UI
↓ Tauri command
Rust command layer
...
Tauri를 선택한 주된 이유는 네이티브 파일 조작을 Rust 측에 맡기면서도, UI는 React/TypeScript로 빠르게 만들 수 있기 때문이었습니다. Electron보다 작은 구조로 만들기 쉬우며, Windows, macOS, Linux를 동일한 코드베이스로 목표할 수 있습니다.
또한, 치환 엔진을 Tauri와 독립된 Rust crate로 분리했습니다. 이를 통해 GUI를 실행하지 않고도 문자 인코딩(Character Encoding), 차이점(Diff), 백업, Undo 기능을 테스트할 수 있습니다.
문자 코드의 자동 판별은 100% 정확하지 않습니다. 따라서 SpeedReplace에서는 디코드되었는지 여부뿐만 아니라, 치환 전에 왕복 변환을 수행합니다.
let text = encoding
.decode_without_bom_handling_and_without_replacement(body)
.ok_or_else(|| Error::Encoding("invalid byte sequence".into()))?
...
원래 바이트열로 완전히 돌아가지 않는 파일은 안전한 치환 대상으로 간주하지 않습니다. 치환된 문자열이 원래의 문자 코드로 표현될 수 없는 경우에도 쓰기를 거부합니다.
현재는 다음 문자 인코딩을 대상으로 합니다.
- UTF-8 (BOM 포함/미포함)
- UTF-16 LE/BE
- Shift_JIS
- EUC-JP
- ISO-2022-JP
- Windows-1252
BOM과 LF/CRLF/CR도 기록하여 가능한 한 원본 형식을 유지합니다.
미리보기가 정확하더라도, '미리보기 이후 파일이 변경된 경우'에는 그 결과를 적용해서는 안 됩니다.
SpeedReplace는 스캔 시점에 치환 전후의 BLAKE3 해시를 보유합니다. 적용 시 파일을 다시 읽어 다음 사항을 확인합니다.
- 현재 해시가 미리보기 입력 해시와 일치하는지
- 규칙 재실행 결과가 미리보기 출력 해시와 일치하는지
- 백업이 원본 해시와 일치하는지
- 쓰기 후 파일이 출력 해시와 일치하는지
위 중 단 하나라도 다르면 충돌(Conflict)로 간주하여 작업을 중단합니다.
쓰기는 같은 디렉토리의 임시 파일을 거쳐 플러시한 후 덮어씁니다. 단순히 fs::write를 사용하는 것보다 구현량은 늘어나지만, 중간 상태가 남는 것을 방지할 수 있습니다.
Undo 기능 역시 무조건적인 덮어쓰기 방식으로는 위험합니다. 치환 후에 사용자가 파일을 편집했다면, Undo는 그 새로운 변경 사항을 지워버릴 수 있기 때문입니다.
따라서 Undo 시에도 현재 해시와 작업 매니페스트(operation manifest)에 기록된 치환 후 해시를 비교합니다. 외부 변경이 감지된 파일은 충돌로 간주하여 건너뜁니다.
공개 전 감사 과정에서는 이 주변부를 더욱 강화했습니다.
- 심볼릭 링크는 검색 시점과 쓰기 직전 두 경우 모두 거부
- Undo의 작업 ID는 정규형 UUID로 제한
- 매니페스트 내 백업 경로는 신뢰하지 않고, 앱 관리 하의 객체 디렉토리에서 해결
- 스키마와 매니페스트 ID를 검증
'정상적인 경우에 되돌리기'뿐만 아니라, 악의적이거나 예상치 못한 상태에서도 어디까지 보수적으로 멈출 수 있는지 테스트했습니다.
화면은 3단계로 구성되어 있습니다.
- 파일 또는 폴더 선택
- 치환 규칙 입력
- 차이점(Diff) 미리보기 및 적용
심플 모드에서는 필요한 최소한의 항목만 표시하고, 상세 모드에서는 여러 규칙, 정규 표현식(Regex), include/exclude glob, 크기 제한 등을 표시합니다.
미리보기 후에 규칙이나 대상이 변경되면, 해당 미리보기는 stale로 간주하여 재스캔할 때까지 적용 버튼을 비활성화합니다.
또한, 바이너리나 문자 인코딩 미확인 등 대응하지 않는 파일은 기본적으로 숨기고, '대응하지 않는 파일 표시'를 통해 확인할 수 있도록 했습니다. 이는 오류의 존재는 남기되, 일반적인 치환 대상에 집중할 수 있도록 조정한 것입니다.
UI 초기 언어는 영어이며, 일본어로 전환 가능합니다. 언어 파일을 추가하는 것만으로 선택지에 나타나도록 Vite의 import.meta.glob을 사용했습니다.
const localeModules = import.meta.glob(
"../locales/*.ts",
{ eager: true },
...
영어 메시지 키를 기준으로, 일본어 측에 Record<TranslationKey, string> 형태를 만족시킴으로써 TypeScript 빌드 단계에서 번역 키 누락을 감지합니다.
SpeedReplace는 .txt, .csv, .md, .json, .php 등 소스 코드나 설정 파일을 다룰 수 있습니다. 다만, 이는 확장자 허용 목록이 아닙니다.
판단 기준은 '대응 문자 인코딩의 일반 텍스트(plain text)로 안전하게 왕복 변환할 수 있는가'입니다. 따라서 확장자가 없는 Dockerfile이나 Makefile, 자체 확장자도 처리할 수 있습니다.
반대로 .docx, .xlsx, .pdf, 압축 파일, 이미지 등의 바이너리/컨테이너 형식은 대상에서 제외됩니다. CSV나 JSON 역시 구문을 해석하는 것이 아니라, 어디까지나 텍스트로 치환합니다.
GitHub Actions를 이용해 macOS, Ubuntu, Windows의 매트릭스 CI를 실행하고 있습니다.
cargo fmt --check<br>cargo clippy -D warnings<br>
- Rust 테스트<br>- TypeScript/Vite 빌드<br>- Tauri 데스크톱 컴파일<br>- npm/RustSec 감사
배포 워크플로우에서는 다음을 생성합니다.
- Windows x64: MSI, NSIS<br>
.exe<br> - Linux x64:<br>
.deb, AppImage
여기서 예상치 못하게 걸린 것이 Tauri의 --bundles 기능이었습니다.
인수(Argument)였습니다. 쉼표로 구분된 값을 GitHub Actions의 매트릭스에서 PowerShell로 전달했을 때, Windows에서 예상한 인수가 아닌 빌드가 실패했습니다.
결국, 매트릭스 값을 다음과 같은 공백으로 구분하는 방식으로 변경하여 Windows와 Linux 양쪽 모두에서 작동하도록 만들었습니다.
- name: Windows
bundles: msi nsis
- name: Linux
...
로컬 환경에서는 발견하기 어려운 '셸에 의한 인수 해석 차이' 때문에, 실제 기기(실물) CI를 거치는 것의 중요성을 다시 한번 확인했습니다.
v1.0.0이 작동하는 즉시 공개할 수도 있었습니다. 하지만 동작 완료와 공개 준비 완료는 별개라고 판단하여, v1.0.1로 공개 전 감사를 진행했습니다.
확인 및 개선한 항목들은 예를 들어 다음과 같습니다:
- 비밀 정보(Secret), 개인 암호(Personal Pass), 전체 기록 스캔
- 의존성(Dependency) 및 라이선스 확인
- npm과 RustSec의 자동 감사(Audit)
- Dependabot 설정
- GitHub Actions를 불변의 커밋 SHA에 고정
SECURITY.md,
CONTRIBUTING.md,
Issue/PR 템플릿 - 전용 아이콘 및 각 OS별 에셋- 릴리스 자산의 SHA-256 체크섬
이 과정에서, 정상적인 기능 추가보다는 심볼릭 링크(Symbolic Link)나 Undo 매니페스트(Undo Manifest)의 신뢰 경계와 같은 부분에 수정이 들어갔습니다.
안정 버전 v1.0.1에서는 Rust 테스트 16건, 3개 OS에서의 빌드, Windows/Linux 배포물 생성을 확인했습니다.
이 개발은 AI 코딩 에이전트와 대화하며 진행되었습니다. 설계 초안, 구현, 테스트, CI, README, 릴리스 노트, 공개 전 감사에 이르기까지 일련의 작업을 동일한 맥락에서 반복했습니다.
하지만 '어떤 앱을 만들 것인가'와 '어디까지를 안전하게 할 것인가'는 자동으로 결정되는 것이 아니었습니다.
인간 측에서 특히 중요했던 판단은 다음과 같습니다:
- speed의 '간편함'은 유지한다
- 속도와 안전성이 충돌할 경우 안전성을 우선시한다
- 문자 코드를 임의로 변환하지 않는다
- 미리보기(Preview)와 Undo를 '부가 기능'이 아닌 중심 설계로 삼는다
- Windows와 Linux 배포물까지 만든다
- Public 공개 전에도 보안과 배포 품질을 다시 검토한다
AI는 구현 속도와 확인 범위를 크게 넓혀주었지만, 제품의 성격을 결정한 것은 이러한 우선순위였습니다.
현재 가능한 기능은 다음과 같습니다:
- 개별 파일 및 폴더의 재귀 스캔(Recursive Scan)
- 일반 문자열, 정규 표현식(Regular Expression), 다중 라인 치환
- 여러 규칙의 순차적 적용
- 통일된 형식의 차이점 미리보기(Diff Preview)
- 문자 코드, BOM(Byte Order Mark), 줄 바꿈 코드 유지
- 검증을 거친 백업과 충돌 감지 기능이 있는 Undo
- 영어/일본어 UI 및 언어 파일 추가를 통한 확장
반면, 다음 항목들은 향후 후보입니다.
- 규칙 프리셋(Rule Preset)
- CLI (Command Line Interface)
- 진행 표시와 취소 기능
- 문자 코드의 명시적인 덮어쓰기
- 백업 유지 기간 설정
- 차이점 청크 단위 선택
- 각 OS별 코드 서명 및 macOS Notarization
특히 서명은 바이너리를 일반 배포하는 데 있어 중요한 남은 작업입니다. 현재 배포물은 미서명이므로, OS가 보안 경고를 표시할 수 있습니다.
일괄 치환 앱의 핵심은 문자열의 replace 처리 자체가 아니었습니다.
어려웠던 점들은 다음과 같습니다:
- 지금 보이는 미리보기와 실제로 작성하는 내용이 동일해야 하는 것
- 원래의 문자 코드나 BOM을 유지하는 것
- 외부 업데이트를 무시하지 않는 것
- 백업과 Undo 자체도 안전해야 하는 것
- 3개 OS에서 동일한 품질 게이트(Quality Gate)를 통과시키는 것
사용자 입장에서는 '검색하고 치환하는' 앱입니다. 그 단순한 조작을 안심하고 실행할 수 있도록 하기 위해, 눈에 보이지 않는 곳에 많은 검증 과정을 넣는 것. 그것이 SpeedReplace 개발에서 가장 큰 주제가 되었습니다.
리포지토리와 릴리스는 다음에서 확인할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기