[메이플 사진관] 자동화 파이프라인으로 유지보수가 필요 없는 시뮬레이터 만들기
자동화 파이프라인으로 유지보수가 필요 없는 시뮬레이터 만들기
이 글의 내용중 일부는 일반인이 이해하기 힘든 내용이 포함되어 있을 sudo 있습니다.
안녕하세요. 메이플 사진관을 개발한 개발자 SpiralMoon 입니다.
오늘은 자동화 파이프라인을 도입해 유지보수가 필요 없는 메이플스토리 말풍선 & 명찰 시뮬레이터를 개발한 과정을 풀어보려고 합니다.
메이플 사진관 - 메이플 프로필, 시뮬, 포토샵 서비스
말풍선, 명찰 시뮬레이터가 출시되었어요!
maplestudio.app
시리즈
2024.09.06 - [Project] - [메이플 사진관] 프로젝트 기획부터 서비스 공개까지
2024.11.25 - [Project] - [메이플 사진관] 기능 업데이트와 유저 이벤트 진행 후기
2025.02.04 - [Project] - [메이플 사진관] 디자인 시스템 도입과 리뉴얼 적용기
2025.05.06 - [Project] - [메이플 사진관] 메이플 포토샵을 만들며
없는 것에서 새로운 단서를 찾다
세상에는 많은 서비스가 있지만 중복된 기능이 많고, 자기들만의 고유한 기능이 없는 서비스는 점점 사람들에게서 잊혀집니다. 그렇기 때문에 대체제가 없거나 남들보다 편리한 서비스만이 살아남고 있으며, 이 시간에도 많은 기업과 서비스가 사라지고 있습니다.
메이플스토리와 관련된 유틸성 웹사이트들도 그렇습니다. 넥슨에서 API를 제공하기 시작했을 때 많은 사이트가 등장했지만, 현시점의 플레이어들은 대체제가 없거나 가장 잘 만들어져있는 사이트만 주로 이용합니다.
저는 메이플 사진관 서비스를 개성있는 서비스로 가꾸기 위해 남들이 아직 만들지 않은 것이 무엇이 있는지 고민했습니다. 아이디어를 구상하던 중 말풍선 & 명찰 시뮬레이터라는 아이디어가 떠올랐고 아직 다른 사이트에서도 지원하지 않는 기능임을 확인했습니다.
잠시 게임 내부의 이야기를 해보겠습니다. 메이플스토리에서는 캐릭터의 외형을 꾸밀 수 있는 기능과 비슷하게 말풍선과 명찰을 꾸밀 수 있습니다. 커스터마이징은 안되지만 미리 만들어진 모양으로 변경할 수 있도록 장착 가능한 반지 아이템의 형태로 존재하며 모든 플레이어가 하나씩은 꼭 착용할 정도로 인기있는 꾸밈 요소중 하나입니다.
저는 여기서 두 가지 불편함을 찾았습니다.
- 캐시샵에서는 판매 기간이 종료된 아이템을 미리보기 할 수 없음
- 유저간 거래소에서는 반지 아이템의 미리보기 기능이 지원되지 않음 (버그 또는 누락으로 추정)
즉, 지난 시즌에만 판매한 반지는 구글링해서 타인이 업로드한 사진으로 보거나 직접 구매하여 장착해보지 않으면 외형 확인이 불가합니다.
과연 내 캐릭터와 어울릴까?
이 반지를 장착했을 때 닉네임 길이에 따른 줄바꿈이 어색하면 어떡하지?
제한된 정보 속에서 플레이어는 위와 같은 불안감을 가지게 되며, 게임의 재미를 반감시키게 됩니다.
이 불편함을 해소하고자 말풍선과 명찰을 시뮬레이션 할 수 있는 기능을 메이플 사진관에 추가하기로 결정했습니다. 그러나 구현에는 시스템상 존재하는 모든 말풍선 & 명찰 반지 아이템에 대한 정보와 Asset이 필요했고, 게임사에서 API로 제공해주는 정보가 아니기에 다른 방법을 모색해야 했습니다. 바로 필요한 데이터를 직접 추출하고 이 과정을 파이프라인으로 구성하는 것입니다.
파이프라인(Pipeline)이란?
우선 파이프라인의 정의를 간략하게 설명하겠습니다.
소프트웨어 개발에서 파이프라인은 한 데이터 처리 단계의 출력이 다음 단계의 입력으로 이어지는 형태의 시스템 구조를 의미합니다. 각 단계는 특정 역할만을 수행하며, 역할을 분리하여 구성하기 때문에 전체 시스템의 유지보수와 재사용성이 매우 뛰어납니다.
가장 흔한 예시로 서버 개발 시 프로그램을 Docker Container 형태로 배포할 때 구성하는 CI/CD 파이프라인이 있습니다.
설계 - 자동화 아키텍처
그렇다면 파이프라인을 어디에 어떻게 적용할 수 있을까요?
메이플스토리는 한 달 주기로 새로운 릴리즈가 업데이트되는 라이브서비스입니다. 이는 매월 새로운 게임 데이터가 추가된다는 뜻이며, 만약 시뮬레이터를 만들었다면 최신 데이터를 지속적으로 반영해야 인게임과 동일한 경험을 제공할 수 있다는 뜻이기도 합니다. 저는 데이터를 수집하고 운반하는 작업을 파이프라인으로 구성해 해결하기로 했습니다.
파이프라인은 게임 클라이언트 파일(Base.wz)로부터 Asset 데이터 추출을 담당하는 extractor와 데이터 변환 및 업로드를 담당하는 uploader로 나누어, 최종적으로는 추출된 Asset이 메이플 사진관의 DB와 File Storage까지 자연스럽게 전달되도록 설계했습니다.
중요한 것이 하나 더 있는데요, 지속성을 유지하고 개발자의 수동 작업을 없애기 위한 시스템 자동화(Automation)입니다.
위 사이클은 메이플 클라이언트의 신규 릴리즈가 감지되면 스스로 실행되도록 자동화를 설정해두어 시뮬레이터를 항상 최신 상태로 유지합니다.
저는 설계를 마무리하고 파이프라인을 구축하기 위해 리서치를 진행했습니다.
개발 - 파이프라인 첫 번째, Asset 추출 (extractor)
파이프라인의 첫 번째 단계는 게임에 존재하는 모든 말풍선과 명찰의 데이터를 찾고 추출하는 것입니다.
메이플스토리 게임 파일 분석
우선 메이플스토리의 게임 데이터가 어떻게 구성되어있는지 간단하게 알아봅시다. 게임 데이터는 WzComparerR2(위컴알)을 통해 뜯어볼 수 있습니다.
데이터 파일은 .wz 확장자이고 게임 클라이언트가 설치된 디렉토리의 /Data 폴더에 위치합니다.
그 하위로는 데이터의 역할에 따라 Character, Item, Map, Sound 등 여러 파일로 분할되어 있습니다. 그리고 Base.wz은 모든 데이터 파일을 하나로 통합하는 Root 역할입니다.
모든 데이터 파일은 여러 노드로 이루어진 Tree 구조입니다. 그리고 하나의 데이터 묶음이 이미지(.img) 단위로 통합되어 있으며, 이미지 내부에는 데이터 속성의 key와 Int32, String, Png, Vector 등의 여러 value 값이 존재합니다.
(여기서 이미지는 사진 파일이 아닙니다!)
위 사진은 Character/Ring/01115735.img에 위치한 어떤 아이템의 기본 정보입니다. 해당 정보를 통해 icon이 .png 확장자임과 어떤 곳을 참조(_outlink)하는지, 어느 nameTag(517)에 대한 정보인지 등을 알 수 있습니다.
미리보기 기능을 사용해 01115735가 "초코곰 명찰 반지"라는 장비 아이템임을 알 수 있었습니다.
이번에는 nameTag = 517이 무엇을 의미하는지 찾아봅시다.
다른 노드인 UI/NameTag.img.517으로 들어가니 해당 명찰의 그래픽 정보가 있습니다. west, center, east 구성을 이루는 .png 조각, 그리고 해당 .png가 그려질 위치 보정값(origin), 마지막으로 명찰의 텍스트 색상인 clr(Integer ARGB)이 있네요.
이것은 인게임에서 해당 명찰 반지를 장착하면 여기에 정의된 정보로 렌더링한다는 점을 시사합니다.
지금까지 분석한 정보를 요약하면 다음과 같습니다.
- 게임 데이터는 Base.wz를 루트로 삼는 트리 구조
- 각 데이터는 역할에 따라 여러 .img에 분산되어 있음
그렇다면 이 정보들을 자동으로 추출하는 방법이 있을까요? 안타깝게도 위컴알은 데이터를 내보내는 기능이 있지만 출력할 데이터를 수동으로 선택하고 버튼을 눌러야하므로 자동화에는 적합하지 않았습니다. 그래서 추출 프로그램을 직접 만들기로 했습니다.
WzComparerR2.WzLib 이해
필요한 정보만을 찾아 추출하는 프로그램을 개발하려면 런타임에서 .wz 파일을 읽을 수 있는 도구가 필요합니다.
다행히도 위컴알 프로그램은 오픈소스로 공개되어 내부에서 사용중인 WzLib라는 이름의 모듈을 찾을 수 있었습니다. WzLib은 .wz 파일의 구조와 정보를 런타임에서 표현하는 다양한 종류의 자료형이 정의된 모듈입니다.
그런데... 사용은 어떻게 하는 것일까요?
위컴알 프로젝트는 예시 코드, 공식문서, 클래스 & 메소드 정의 주석을 찾을 수 없었습니다. 그나마 가끔 한줄씩 있는 주석마저 중국어로 되어있었습니다. 10년째 개발되고 있는 프로그램이고 소스코드도 수십만줄에 달했기 때문에 모든 코드를 찾아보며 사용법을 익히는 방법도 좋은 전략은 아니었습니다.
(아마도 WzLib가 위컴알 프로젝트만을 위한 모듈이다보니 굳이 주석을 작성하지 않은 것 같습니다.)
우선 저 많은 목록에서 실질적으로 사용하게 될 클래스만 추려내어 분석 범위를 좁혔습니다.
- Wz_Structure
- Wz_Image
- Wz_Node
- Wz_Png
- Wz_Vector
Claude AI에게 위 클래스 목록에 대한 개요 생성을 지시했고, 저는 WzLib을 활용한 간단한 예시 스크립트를 작성 + 디버깅해보며 AI가 생성한 사용법과 교차 검증을 실시했습니다. 그리고 틀린 부분을 하나씩 교정해나가면서 최종적으로는 사용법에 대한 문서화를 완료하고 사용법을 익혔습니다.
말풍선 & 명찰 반지 Asset을 파일로 출력하기
데이터 파일 구조와 추출 방법을 이해하였으므로 바로 추출 프로그램 작성을 위한 프로젝트를 생성했습니다.
WzLib가 C#으로 개발된 모듈이라 제 프로젝트도 C#으로 구성하였고, WzLib가 NuGet 패키지 저장소에 공개되지 않은 패키지라서 repository에 git submodule로 등록 후 프로젝트 설정에서 WzLib을 의존성으로 수동 추가했습니다.
추출해야하는 Asset은 여러 .img에 흝어져 있었습니다.
데이터 위치 | 추출해야하는 정보 |
Character/Ring/01112100 ~ 01112299.img Character/Ring/01115000 ~ 01115999.img |
말풍선 & 명찰 반지 아이템 기본 정보 - 이미지 식별 코드 - 아이템 아이콘 - 말풍선 또는 명찰 식별 코드 - 애니메이션 적용 여부 |
UI/ChatBalloon.img.{chat balloon code} | 말풍선 그래픽 정보 - 말풍선 이미지 slice 조각 (head, nw, n, ne, w, c, e, sw, s, se, arrow) - 말풍선 이미지 slice의 보정 좌표 vector (x, y) |
UI/NameTag.img.{name tag code} | 명찰 그래픽 정보 - 명찰 이미지 slice 조각 (w, c, e) - 명찰 이미지 sprite - 명찰 이미지 slice의 보정 좌표 vector (x, y) |
String/Eqp.img/Eqp/Ring/{img code} | 말풍선 & 명찰 반지 아이템 정보 (텍스트) - 아이템 명 - 아이템 설명 |
반지 아이템의 기본 정보를 우선적으로 조회하여 식별자를 찾고, 말풍선이냐 명찰이냐에 따라 다른 .img에서 그래픽 정보를 찾게되며 최종적으로는 반지 아이템 1개당 3개의 .img를 조회하게 됩니다.
다음은 소스코드로 구현된 Asset 추출 과정입니다. (일부 코드 생략)
using Microsoft.Extensions.Configuration;
using RingAssetsExtractor;
using System.Drawing;
using WzComparerR2.WzLib;
string wzFilePath = "C:\\Nexon\\Maple\\Data\\Base\\Base.wz"; // Base.wz file path
Wz_Structure wz = new Wz_Structure(); // Root WZ tree
// wz 열기 생략
Wz_File wzFile = wz.WzNode.GetNodeWzFile(); // Root WZ file
#region .img 로딩
Wz_Node imgNodeChatBalloon = wz.WzNode.FindNodeByPath("UI\\ChatBalloon.img");
Wz_Node imgNodeNameTag = wz.WzNode.FindNodeByPath("UI\\NameTag.img");
Wz_Node imgNodeString = wz.WzNode.FindNodeByPath("String\\Eqp.img");
Wz_Image imgChatBalloon = imgNodeChatBalloon.GetNodeWzImage();
Wz_Image imgNameTag = imgNodeNameTag.GetNodeWzImage();
Wz_Image imgString = imgNodeString.GetNodeWzImage();
imgChatBalloon.TryExtract();
imgNameTag.TryExtract();
imgString.TryExtract();
#endregion
#region 추출 범위에 해당하는 반지 .img 필터링
// 말풍선 & 명찰 반지에 해당되는 식별 코드 범위 정의
List<(int, int)> ringEqpCodeRanges = new([
(1112100, 1112299),
(1115000, 1115999)
]);
Wz_Node nodeRing = wz.WzNode.FindNodeByPath("Character\\Ring");
// 모든 반지 데이터 중 위에서 정의한 범위에 해당되는 반지만 필터링
List<Wz_Node> imgNodeRings = nodeRing.Nodes.Where((node) =>
{
// ex) 01112100.img
string imgName = node.Text;
if (!imgName.EndsWith(".img") || imgName.Length != 12)
{
return false;
}
if (int.TryParse(imgName.Substring(0, 8), out int eqpCode))
{
return ringEqpCodeRanges.Any(group => group.Item1 <= eqpCode && eqpCode <= group.Item2);
}
return false;
}).ToList();
#endregion
Output output = new Output(); // .json 저장 스키마
#region 반지 정보 파싱 및 .png 이미지 저장
try
{
foreach (var imgNodeRing in imgNodeRings)
{
Wz_Image imgRing = imgNodeRing.GetValue<Wz_Image>();
string ringEqpCode = imgRing.Name.Substring(0, 8); // ex) 01112100
// Load
if (imgRing.TryExtract())
{
Wz_Node nodeRingInfo = imgRing.Node.Nodes["info"]; // summary info node
// icon node
Wz_Node nodeIconRaw = nodeRingInfo.Nodes["iconRaw"];
Wz_Node nodeIconRawCanvas = nodeIconRaw.GetLinkedSourceNode(wzFile);
Wz_Png pngIconRaw = nodeIconRawCanvas.GetValue<Wz_Png>();
// 아이템 아이콘을 .png로 저장
pngIconRaw.SaveToPng(Path.Combine(imagesDir, $"{imgNodeRing.Text}.iconRaw.png"));
Wz_Node nodeChatBalloon = nodeRingInfo.Nodes["chatBalloon"];
Wz_Node nodeNameTag = nodeRingInfo.Nodes["nameTag"];
bool isChatBalloon = false;
bool isNameTag = false;
int ringRefCode = 0; // ring ref eqpCode
string type = null; // "ChatBalloon" or "NameTag"
Wz_Node nodeRingGraphic = null;
if (nodeChatBalloon != null)
{
isChatBalloon = true;
ringRefCode = nodeChatBalloon.GetValue<int>();
type = "ChatBalloon";
nodeRingGraphic = imgChatBalloon.Node.Nodes[$"{ringRefCode}"];
}
if (nodeNameTag != null)
{
isNameTag = true;
ringRefCode = nodeNameTag.GetValue<int>();
type = "NameTag";
nodeRingGraphic = imgNameTag.Node.Nodes[$"{ringRefCode}"];
}
// 그래픽 정보가 없으면 스킵 (default chat balloon, default imgName tag, etc.)
if (nodeRingGraphic == null)
{
continue;
}
bool isAnimationRing = nodeRingGraphic.HasAnimation();
Ring ring = new Ring();
ring.EqpCode = ringEqpCode;
ring.Type = type;
ring.RingCode = ringRefCode.ToString();
ring.IsAnimation = isAnimationRing;
// nameTag : w, c, e
// chatBalloon : head, nw, n, ne, w, c, e, sw, s, se, arrow
string[] sliceNodeNames = { "head", "nw", "n", "ne", "w", "c", "e", "sw", "s", "se", "arrow" };
// 애니메이션이 적용된 반지 (모든 프레임 저장)
if (isAnimationRing)
{
// 생략
}
// 애니메이션이 적용되지 않은 반지 (기본)
else
{
List<Wz_Node> nodeSlices = nodeRingGraphic
.Nodes
.Where(n => sliceNodeNames.Contains(n.Text))
.ToList();
foreach (Wz_Node nodeSlice in nodeSlices)
{
ImageData slice = new();
if (nodeSlice.GetValue<Wz_Png>() != null)
{
Wz_Node nodeSliceCanvas = nodeSlice.GetLinkedSourceNode(wzFile);
Wz_Png pngSlice = nodeSliceCanvas.GetValue<Wz_Png>();
string slicePngName = $"{nodeSlice.FullPath.Replace("\\", ".")}.png";
// 이미지 slice 조각을 .png로 저장
pngSlice.SaveToPng(Path.Combine(imagesDir, slicePngName));
Bitmap bitmap = pngSlice.ExtractPng();
slice.Size = new()
{
Height = bitmap.Height,
Width = bitmap.Width,
};
}
// 이미지 slice 조각의 위치 보정 vector 추출
foreach (Wz_Node nodeSliceProperty in nodeSlice.Nodes)
{
var key = nodeSliceProperty.Text;
var vector = nodeSliceProperty.GetValue<Wz_Vector>();
if (key == "origin" && vector != null)
{
var x = vector.X;
var y = vector.Y;
Origin origin = new()
{
X = x,
Y = y
};
slice.Origin = origin;
}
}
ring.Images.Add(nodeSlice.Text, slice);
}
}
// 렌더링 시 텍스트 색상 추출
Wz_Node nodeClr = nodeRingGraphic.Nodes["clr"];
string color = $"#{nodeClr.GetValue<int>():X8}"; // Convert integer to ARGB hex
// 반지 아이템의 이름, 설명 추출
Wz_Node nodeRingString = imgString.Node.FindNodeByPath($"Eqp\\Ring\\{int.Parse(ringEqpCode)}");
string name = nodeRingString.Nodes["name"]?.GetValue<string>() ?? "";
string desc = nodeRingString.Nodes["desc"]?.GetValue<string>() ?? "";
ring.Color = color;
ring.Name = name;
ring.Description = desc;
output.Rings.Add(ring);
}
}
}
catch (Exception e)
{
Console.WriteLine("Exception occured.");
Console.WriteLine(e);
return;
}
#endregion
// .json 출력 생략
.wz 파일을 열고 추출 대상이 들어있는 .img를 미리 로딩합니다. 그리고 Character/Ring에서 말풍선 & 명찰 반지에 해당하는 반지 .img만 필터링합니다. 이 때 반지 .img 1개당 반지 아이템 1개를 의미하며, png를 파일로 저장하고 메타데이터를 파싱하여 객체에 저장하는 과정을 반복합니다.
위에서 작성한 스크립트를 실행하면 추출 대상의 정보를 여러개의 png 파일과 하나의 json 파일로 출력합니다.
// ring.json
{
"rings": [
{
"eqp_code": "01115735",
"type": "NameTag",
"ring_code": "517",
"name": "초코곰 명찰 반지",
"desc": "캐릭터 이름이 초코곰 명찰에 나타난다. ",
"color": "#FFFFE8F4",
"is_animation": false,
"images": {
"w": {
"origin": {
"x": 36,
"y": 5
},
"size": {
"w": 36,
"h": 22
}
},
"c": {
"origin": {
"x": 0,
"y": 5
},
"size": {
"w": 1,
"h": 22
}
},
"e": {
"origin": {
"x": 0,
"y": 5
},
"size": {
"w": 36,
"h": 22
}
}
}
},
...
]
}
Asset 추출기의 전체 소스코드는 아래 GitHub에 게시하여 모두가 실행해 볼 수 있도록 오픈소스로 공개하였습니다.
GitHub - SpiralMoon/maplestory-ring-assets-extractor: MapleStory ring assets extractor for chat balloon and name tag.
MapleStory ring assets extractor for chat balloon and name tag. - SpiralMoon/maplestory-ring-assets-extractor
github.com
(WzLib 사용 사례를 찾아볼 수 없어 개발이 힘들었던 점 때문에... 언젠가 WzLib를 찾는 또 다른 개발자의 도움이 되길 바라며...)
개발 - 파이프라인 두 번째, Asset 업로드 (uploader)
파이프라인의 두 번째 단계는 앞단계에서 출력된 output 데이터를 정제하여 메이플 사진관의 DB, AWS S3에 업로드하는 것입니다.
이미지 파일은 S3에, 메타데이터는 DB에 저장하여 시뮬레이터에 API 형태로 제공합니다. images 컬럼에는 해당 반지의 .png 파일이 저장된 S3 URL을 포함시켰습니다.
개발 - 시뮬레이터 개발
앞서 파이프라인에 대한 내용을 많이 서술했습니다만, 결국 사용자에게 제공되는 것은 시뮬레이터이므로 직관적이고 쉽게 사용할 수 있는 기능을 만들어야 합니다.
말풍선 & 명찰 컴포넌트
<MapleStoryChatBalloon code={'01115632'} text="말풍선 컴포넌트입니다." />
<MapleStoryNameTag code={'01115712'} text="MAPLESTORY" />
식별자와 문자열을 파라미터로 간단하게 사용할 수 있는 컴포넌트를 정의했습니다.
컴포넌트 내부에서 API를 호출하여 식별자에 해당하는 반지의 데이터를 조회하고, 입력된 텍스트와 이미지를 canvas 상에서 조립하여 렌더링합니다.
어떤가요? 인게임과 거의 동일한 수준으로 재현했습니다.
이미지 조각의 보정 좌표를 반영하는 로직과 가변 영역의 확장 로직이 컴포넌트 개발의 핵심입니다.
말풍선의 경우 9-slices 구조에 head와 arrow가 추가되어 최대 11개의 이미지 조각이 렌더링에 사용됩니다. 이때 조각별 보정 좌표가 다르게 적용되며, nw부터 렌더링을 시작하기 때문에 나머지 조각들은 보정 좌표에 현재 렌더링 상태의 너비와 높이를 고려한 계산이 추가적으로 발생합니다.
n, w, c, e, s 영역은 말풍선의 텍스트 분량에 따라 길이가 가변적으로 변합니다. 늘어난 영역은 패턴형 이미지 케이스를 고려해 타일링(Tiling) 기법으로 처리하였습니다.
좌측과 우측의 길이가 다른 엣지 케이스 때문에 수식이 좀 더 복잡해진 것이 어려웠던 것 같습니다. (단순 중앙 정렬형 렌더링 불가)
인게임과 동일한 사용자 경험 제공하기
시뮬레이터를 처음 사용해도 직관적이고 인게임과 비슷한 느낌을 제공하기 위해 고민했습니다.
반지 항목 위에 마우스를 올리면 인게임과 동일한 툴팁을 출력하여 게임에서 학습된 유저 경험이 시뮬레이터에서도 자연스럽게 이어지도록 했습니다.
범용적으로 사용할 수 있도록 아이템 툴팁 컴포넌트를 사전에 미리 개발해 둔 것이 큰 도움이 되었습니다. 툴팁을 도입하기 이전에는 사용처가 없어 개발 진행 여부에 고민이 있었지만, 적용하고나니 어색하지 않고 다른 기능에도 적극 활용할 수 있다는 생각이 들었습니다.
마무리하며
저는 이번 과정에서 게임 파일 분석이 들어간 다소 특이한 파이프라인을 구축한 경험이 정말 즐겁고 유익했다고 생각합니다. 메이플스토리는 국내 1위 RPG 게임이지만, 자신의 소스코드에서 WzLib로 메이플스토리 내부를 들여다 본 개발자가 과연 국내에 몇이나 될까요? 이 기술로 제작이 가능한 다른 아이디어가 떠오르기 시작했고 다음 계획에 추가했습니다.
이번 시뮬레이터가 타 사이트에는 없는 기능인 덕인지 공개 첫날부터 많은 유저분들께서 찾아와주셨습니다. 공개 직후 24시간 동안 반지 아이템을 클릭한 횟수는 약 22,000회입니다. 아직 커뮤니티에 홍보글 1개만 작성한 상태인데 2주가 지난 지금도 어느정도 이용량이 나오는 것을 보니 누군가에게는 확실히 도움이 되고 있네요. 뿌듯합니다.
Perl 언어를 개발한 Larry Wall은 프로그래머가 갖춰야 할 3대 덕목(?) 중 첫 번째로 나태(Laziness)를 이야기합니다.
전체적인 노력을 줄이기 위해 수고를 아끼지 않는 기질. 다른 사람들에게 유용하다고 생각하는 노동력을 절감하는 프로그램을 작성하며, 같은 질문에 여러번 답할 필요가 없도록 내용을 문서화한다. 이것이 바로 프로그래머의 첫 번째 미덕이다.
간단히 말하면 게으름 = 귀찮은 일을 자동화하려는 욕구로 이어진다는 뜻입니다. (단, AI 딸깍은 예외)
저는 오늘도 자동화를 통해 나태라는 덕목을 쌓았습니다.
읽어주셔서 감사합니다.