벌써 2025년의 4분의 1이 다되어간다🧐
뭘 했다고 벌써 3월인가, 졸업을 하면서 빡센 취준을 하겠노라 다짐한 것이 무색하게 3개월동안 제자리걸음만 한 느낌이다.
올해 목표를 되새길 겸, 24년 12월 말에 시작해서 25년 1월 1일에 배포한 개인 프로젝트 회고를 해보려고 한다.
💫@namespace
올해 1월 1일, 간단하게 개인 프로젝트를 만들어서 배포를 했다. 서비스명은 @namespace, 사용자가 입력한 이름으로 고유 별자리를 만들어주는 서비스이다.
💫프로젝트의 시작
사실 @namespace는 24년 7월 쯤, 파이썬으로 간단하게 코드를 짜고 '명로'라고 이름을 붙여놓은 상태였다.
명로, 내 이름으로 만드는 나만의 심볼💫
내 이름으로 만드는 심볼, 로고, 사인, 별자리.
velog.io
창업 활동과 프로젝트를 하면서 매번 반복되는 로고 제작에 지쳐, 키보드를 활용한 기믹을 자주 사용하게 되었다. 텍스트를 키보드 배열을 기준으로 연결하여, 심볼을 만드는 형태였다. 처음에는 피그마에 키보드 이미지를 붙여넣고 선툴로 하나하나 잇는 수동 시스템이었는데, 이것도 겹치는 문자를 고려하면 꽤나 번거로웠다. 이 과정을 자동화하기 위해 코랩에서 파이썬으로 로직을 구현했다.
colab에서 실행한 모습:

💫develop하기
7월에 코드를 작성해놓고, 필요할 때마다 colab에서 이름 부분을 수정해서 돌려쓰곤 했는데, 불편하기도 하고, UI를 입혀서 이쁘게 만들어보고 싶다는 욕심이 항상 있었다. 생성된 패턴이 별자리와 유사한 형태를 띠고 있어 별자리 테마로 시각화하면 좋겠다고 생각하고 있었다.
별 하면 생각나는 것!
별 하나에 추억과 별하나에 사람과 별하나에 소원도 꿈도 아무튼 희망찬 느낌이 들지 않은가. 마침 새해가 다가오기도 하겠다, 이름으로 만드는 별자리에 새해 목표를 담아 카드를 만들어보자며 기획을 시작했다. 사실 필요에 의한 기획이라기보단 카드 UI와 반짝거리는 것, 내 취향만 가득 담은 욕망의 항아리 같은 프로젝트이긴 했다. 카드 스타일 선택, 색상 변경 등 기능 한 두개 추가하여 기획을 마치고, 개발을 시작했다.
💫기능
접속한 페이지에서 이름과 목표를 입력하면, 카드를 만들어준다. 카드는 커스텀 가능하다.
- 한, 영 이름 지원: 한글 자모음 분리해서 한글, 영어 모두 생성 가능하다.
- 카드 색상 변경 가능: 12개 그라데이션 배경
- 카드 스타일 변경 가능: 4가지 패턴
- 별자리 재생성: 생성된 별자리가 마음에 들지 않을 경우 1회에 한해 변경 가능
- 입력한 문자가 키보드 상에서 일렬로 배치된 경우나, 완성된 결과물이 이쁘지 않은 경우가 있다. 이런 경우를 위해 qwery 키보드 배열, dvorak 키보드 배열 2 종류를 사용했다. 따라서 하나의 이름 당 2개의 결과물이 나올 수 있음.
- 이미지 저장: PNG 저장 지원


React와 Typescript를 사용하여 개발했고, framer motion으로 애니메이션을 적용했다. 스타일링은 애증의 tailwind 사용. SVG를 활용한 별자리 렌더링에 특히 정말 많은 공을 들였다.
❌어려웠던 점
파이썬으로 로직을 써놨으니 자바스크립트로 바꾸기만 하면 되겠지! 라며 안일하게 생각했는데, 막상 개발을 시작하니 이런저런 문제에 맞닥뜨렸다.
❌한글 자모음 분해 알고리즘
가장 문제 되었던 부분은 한글 인식 부분이었다. 파이썬으로 코드 작성 시에는, 영어를 기준으로만 작성했기에 한글에 대한 처리가 필요했다. 영어 알파벳처럼 한글도 입력하는 순서대로 문자 하나하나 키보드 위치로 매핑하려고 했으나, 실행을 해보니 계속해서 점 하나만 찍혔다.
why?
원인은 한글과 영어의 문자 체계 차이였다. 영어는 각 알파벳이 독립적인 문자로 취급되지만, 한글은 초성, 중성, 종성의 조합으로 이루어진 복합 문자이다. 이 때문에 "다"를 입력하면 ㄷ + ㅏ 로 인식하지 않고 하나의 문자로 인식하여 키보드 상의 한 위치에만 점이 찍히게 된 것. 의도대로라면 "가나다"를 입력했을 때 중복 문자 제외하고 ㄱ, ㄴ, ㄷ, ㅏ 위치에 점이 찍혀야 하는데, "가", "나", "다"로 인식해 세 개의 점이 찍히게 되는 것이었다. 어떤 이름을 입력하든 세 개의 점만 생성되기 때문에, 별자리로서의 의미가 많이 퇴색되었고 분리 작업이 필요했다.
자모음을 분해한 후에도 이를 키보드 위치로 매핑하는 과정에서 문제가 발생했다. "ㄲ", "ㅘ"와 같은 문자는 어떻게 처리할지, 종성이 있는 글자와 없는 글자를 어떻게 일관되게 처리할지에 대한 문제도 해결해야 했다.
solution
다음 방법으로 한글 입력 문제를 해결했다.
1. 한글 유니코드 분석: 한글 유니코드 체계를 분석하여, 초성 중성, 종성을 분리하는 알고리즘을 구현했다. 이 코드를 통해 "가" 형태의 글자를 "ㄱ" 와 "ㅏ"로 분해할 수 있게 되었다.
function decomposeHangul(char: string): string[] {
const charCode = char.charCodeAt(0);
// 한글 유니코드 범위 체크 (가 ~ 힣)
if (charCode < 0xac00 || charCode > 0xd7a3) {
return [char];
}
const offset = charCode - 0xac00;
const jong = offset % 28;
const jung = ((offset - jong) / 28) % 21;
const cho = ((offset - jong) / 28 - jung) / 21;
// 초성 목록
const chosung = [
"ㄱ", "ㄲ", "ㄴ", "ㄷ", "ㄸ", "ㄹ", "ㅁ", "ㅂ", "ㅃ", "ㅅ", "ㅆ",
"ㅇ", "ㅈ", "ㅉ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ",
];
// 중성 목록
const jungsung = [
"ㅏ", "ㅐ", "ㅑ", "ㅒ", "ㅓ", "ㅔ", "ㅕ", "ㅖ", "ㅗ", "ㅘ", "ㅙ",
"ㅚ", "ㅛ", "ㅜ", "ㅝ", "ㅞ", "ㅟ", "ㅠ", "ㅡ", "ㅢ", "ㅣ",
];
// 종성 목록
const jongsung = [
"", "ㄱ", "ㄲ", "ㄳ", "ㄴ", "ㄵ", "ㄶ", "ㄷ", "ㄹ", "ㄺ", "ㄻ",
"ㄼ", "ㄽ", "ㄾ", "ㄿ", "ㅀ", "ㅁ", "ㅂ", "ㅄ", "ㅅ", "ㅆ", "ㅇ",
"ㅈ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ",
];
const result = [chosung[cho], jungsung[jung]];
if (jong > 0) result.push(jongsung[jong]);
return result;
}
한글 유니코드는 '가'(0xAC00)부터 시작하여 초성 19개, 중성 21개, 종성 28개의 조합으로 이루어져 있다. 따라서 문자의 유니코드 값에서 '가'의 유니코드 값을 빼면 해당 문자의 상대적 위치를 알 수 있고, 이를 분해하여 초성 중성 종성의 인덱스를 구할 수 있다.
예를 들어 "한"이라는 글자를 분해할 경우:
1. '한'의 유니코드에서 '가'의 유니코드 값을 뺀다.
2. 이 값을 28로 나눈 나머지가 종성 인덱스
3. 나머지를 28로 나눈 목을 21로 나눈 나머지가 중성 인덱스
4. 그 몫을 21로 나눈 몫이 초성 인텍스
이렇게 분해된 자모음은 배열로 반환하여 별자리 생성에 사용할 수 있게 된다.
2. 키보드 매핑 구현: 분해된 자모음을 영문 키보드 위치로 매핑하여 영문 이름과 동일한 로직으로 별자리가 생성되도록 작성했다.
// 한글 자모음을 영문 키보드 값으로 매핑
const koreanToEnglish: { [key: string]: string } = {
ㅂ: "Q", ㅈ: "W", ㄷ: "E", ㄱ: "R", ㅅ: "T",
ㅛ: "Y", ㅕ: "U", ㅑ: "I", ㅐ: "O", ㅔ: "P",
ㅁ: "A", ㄴ: "S", ㅇ: "D", ㄹ: "F", ㅎ: "G",
ㅗ: "H", ㅓ: "J", ㅏ: "K", ㅣ: "L",
ㅋ: "Z", ㅌ: "X", ㅊ: "C", ㅍ: "V", ㅠ: "B",
ㅜ: "N", ㅡ: "M",
};
복합 자모음의 경우 이 테이플에 포함되어 있지 않다. 이런 경우에는 decomposeHangul 함수가 반환한 자모음 배열에서 해당 자모음을 찾고, 테이블에 없는 자모음일 경우 별자리 생성에서 제외된다.
function getUniqueCoordinates(
name: string,
keyCoordinates: Record<string, Coordinate>
): Coordinate[] {
const coordinates: Coordinate[] = [];
// 한글을 자모음으로 분해하고 영문으로 변환
name.split("").forEach((char) => {
const decomposed = decomposeHangul(char);
decomposed.forEach((jamo) => {
const englishChar =
koreanToEnglish[jamo]?.toUpperCase() || jamo.toUpperCase();
if (keyCoordinates[englishChar]) {
coordinates.push(keyCoordinates[englishChar]);
}
});
});
return coordinates;
}


❌SVG로 별자리와 패턴 구현하기
별자리를 시각적으로 표현하기 위해 SVG를 사용했는데, 처음에는 단순히 점과 선으로 별자리를 표현하려고 했다. 이렇게 생성된 별자리는 정말 이쁘지 않았고, 별자리라는 느낌도 없었다. 카드 형태도 단색 카드보다는 패턴을 부여해서 화려하게 만들고자 했는데, SVG로 이를 구현하는 것이 정말 어려웠다.
- 밋밋함: 점과 선만으로는 의도한 별자리 느낌이 나지 않았고, 점의 개수가 적을 경우 더욱 밋밋해지는 문제가 있었다.
- 패턴 생성: 사용자 입력에 따라 별자리 모양이 달라지기에 점과 선을 매번 새로 그려야했다. SVG 요소를 프로그래밍 방식으로 생성하고 배치하는 것이 너무 복잡했다. 점과 선 뿐만 아니라 원, 타원, 방사형 패턴 등 다양한 기하학적 요소를 조합해야 하는 어려움이 있었다.
solution
1. 별자리 먼지 추가: 별자리 연결선 외에도, 배경에 작은 별들을 랜덤하게 생성하여 우주 느낌을 강화했다. 이 별 먼지 효과는 매번 다른 패턴으로 생성되어 같은 이름이라도 다른 느낌을 줄 수 있다.
// 랜덤한 작은 별들 생성
const starDust = Array.from({ length: 180 }, (_) => {
const angle = Math.random() * Math.PI * 2;
const radius = Math.random() * 75 + 15;
const x = radius * Math.cos(angle);
const y = radius * Math.sin(angle);
const opacity = Math.random() * 0.4 + 0.1;
const size = Math.random() * 0.6 + 0.2;
return { x, y, opacity, size };
});
이 코드는 180개의 작은 별을 생성한다(이것저것 다 해봤는데 180이 제일 적당했움). 각 별은 중심점을 기준으로,각도와 반지름을 사용하여 위치를 결정한다. 이렇게 하면 별들이 중심을 기준으로 원형으로 분포하게 된다. 각 별들의 투명도와 크기도 랜덤하게 설정하여, 자연스러운 우주 배경을 연출했다.



2. 별자리와 4개의 패턴 구현 로직:
별자리 생성
사용자 이름의 각 문자를 키보드 위치 기반 좌표로 변환 -> 생성된 좌표를 SVG 좌표계로 변환하고, 중심점을 조정하여 별자리가 화면 중앙에 위치하도록 설정 -> 변환된 좌표를 기반으로 별과 별 사이를 연결하는 선을 표시
별이되는 점은 일정 규칙에 따라 각각 크기가 다르게 설정하여, 실제 별자리와 유사하게 보이도록 표현하였다.
// src/components/ConstellationCanvas.tsx
{/* 별자리 선 그리기 */}
{coordinates.length > 1 &&
coordinates.slice(1).map((coord, index) => {
const prevCoord = coordinates[index];
return (
<line
key={`line-${index}`}
x1={(prevCoord.x - xOffset) * scale}
y1={(prevCoord.y - yOffset) * scale}
x2={(coord.x - xOffset) * scale}
y2={(coord.y - yOffset) * scale}
strokeWidth={strokeWidth}
style={{ stroke: "url(#constellationGradient)" }}
/>
);
})}
{/* 별자리 점 그리기 */}
{coordinates.map((coord, index) => (
<g key={`star-${index}`} filter="url(#glow)">
<circle
cx={(coord.x - xOffset) * scale}
cy={(coord.y - yOffset) * scale}
r={dotRadius * starSizes[index]}
fill="url(#constellationGradient)"
/>
</g>
))}
기본 패턴
기본 패턴은 두 개의 원가 방사형 선으로 구성했다. 2시 방향과 8시 방향에 장식을 추가하였다(타로 카드 느낌을 원했움)
12개의 방사선을 30도 간격으로 배치하고, 그 중 2시 방향과 8시 방향 선은 더 길게 연장했다. 두 개의 원(반지름 45, 64)를 그리고, 연장된 방사선의 중간 지점에 초승달과 원 모양의 장식을 추가했다.
{Array.from({ length: 12 }, (_, i) => {
const angle = (i * 30 - 90 + 15) * (Math.PI / 180);
const normalEnd = 64;
const extendedEnd = normalEnd * 1.5;
const extensionMidPoint = normalEnd + (extendedEnd - normalEnd) * 0.5;
return (
<g key={`symbol-${i}`}>
{/* 2시 방향 초승달 (i=1) */}
{i === 1 && (
<path
transform={`translate(${
extensionMidPoint * Math.cos(angle)
} ${
extensionMidPoint * Math.sin(angle)
}) rotate(225)`}
두 번째 패턴
두 개의 원 사이에 1~12까지의 라틴 숫자를 표시하였다.
{/* 로마 숫자 배치 */}
{ROMAN_NUMERALS.map((numeral, i) => {
const angle = (i * 30 - 90) * (Math.PI / 180);
const radius = 54.5; // 두 원 사이의 중간 지점
const x = radius * Math.cos(angle);
const y = radius * Math.sin(angle);
return (
<text
key={`numeral-${i}`}
x={x}
y={y}
textAnchor="middle"
dominantBaseline="middle"
fontSize="6"
fill="url(#constellationGradient)"
style={{ fontFamily: "serif" }}
>
{numeral}
</text>
);
})}
ROMAN_NUMERALS 배열에 저장된 로마숫자를 순회하면서 각 숫자를 배치하며, 각 숫자는 30도 간격으로 배치된다. 시작 각도를 -90으로 설정하여 12시 방향부터 시작하도록 했다.
세 번째 패턴
두 개의 타원을 생성하고, 각각 45도와 135도로 회전시켜 x자 형태로 교차하도록 설정하였다.
{pattern === "ellipse" && (
<>
{/* 첫 번째 타원 - 45도 회전 */}
<ellipse
cx="0"
cy="0"
rx="60"
ry="40"
fill="none"
strokeWidth="1"
style={{ stroke: "url(#constellationGradient)" }}
transform="rotate(45)"
/>
{/* 두 번째 타원 - 135도 회전 */}
<ellipse
cx="0"
cy="0"
rx="60"
ry="40"
fill="none"
strokeWidth="1"
style={{ stroke: "url(#constellationGradient)" }}
transform="rotate(135)"
/>
</>
)}
네 번째 패턴
추가적인 장식은 없으나, 별자리가 더 크게 나타나도록 설정하였다.
// 패턴이 basic일 때는 scale을 1.7배로 증가
const baseScale = 9;
const scale = pattern === "basic" ? baseScale * 1.7 : baseScale;
패턴을 이미지로 만들어 적용하지 않고, 생 SVG 코드로 쓰려니 어려움이 많았다. 위치 계산이 정말 힘들었다. 원형으로 배치되는 요소들(방사선, 로마숫자)는 제대로 배치하기 위해 많은 계산이 필요했다. 그냥 각도 바꿔 돌리면 되는거 아닌가 생각했는데, 의도한대로 배치하기 위해서는 각도, 반지름, 회전 등 다양한 수학적 계산을 적용해야 했다. 균등하게 돌린다고 끝이 아니고, 안정감을 위해 미세 조정을 위해서도 많은 시간이 소요되었다. 끊임없는 계산과 반복작업...cursor가 아니었다면 해내지 못했을 것. thanx to cursor
🚀결과

💡느낀 점
사실 이렇게까지 날 것의 SVG를 다룰 필요가 있었나 싶긴 하지만, 좋은 경험이었던 것 같다. SVG의 기본 요소부터 복잡한 패턴까지 질리도록 다뤘기 때문이다. 그래픽 요소를 배치하면서 수학 능력도 참 많이 늘은 것 같다.......🙃 좌표 계산, 동적 요소 생성 등 다른 프론트 프로젝트에서도 유용하게 사용할 수 있을 것 같다. 한글 자모음 분해 알고리즘을 구현하면서 한영 문자 간 차이와 문자열 조작에 대한 것도 많이 배웠다.
💡배포에서 배운 것
vercel analytics를 적용해 사용자 데이터를 수집해보았다. 친구가 규모 큰 엽합 동아리랑 스토리에 홍보를 해줘서 조금 기대해보았건만, 유입은 매우 적었다. 마지막으로 확인했을 때 40 정도. 첫 페이지와 두번째 페이지 간 이탈률도 높앗다. 첫 페이지를 단순 인풋으로 구현한 것이 이탈의 가장 큰 원인이 아닐까 추측한다. 첫 페이지의 UI 가 직관적이지 않고, 서비스의 목적도 명확하지 않아 보이기 때문이다. 서비스의 첫 인상과, 사용자 경험에 대해서 많은 고민을 했다.
물론 서비스에 대한 니즈가 없는 것도 큰 한몫을 했을 것이다. 이 경험을 통해 서비스 기획 단계에서 타겟 사용자와 니즈, 서비스 방향성을 명확하게 정의해야한다는 것을 크게 느꼈다.
소소한 사용자지만 사용자 데이터를 보고, 서비스의 부족한 점을 많이 고민해볼 수 있었다. 서비스의 가치를 첫 화면에서 명확하게 전달하기. 사용자의 호기심과 참여를 이끌어낼 수 있는 요소가 중요하다고 나름 결론내어 보았다.
💡아쉬운 점
서비스를 완성하고 한참이 지났지만, 아직도 아쉬운 부분이 계속 눈에 띈다.
1. 사용자 경험과 인터렉션
처음 머릿속에서 그림을 그릴 땐 다양한 인터렉션을 상상했지만, 시간에 쫒겨 2페이지의 단순한 구조로 마무리했다. 별자리 생성 후 색 변경이나 패턴 변경 기능을 넣긴 했으나, 사용자가 적극적으로 참여할 수 있는 인터렉션은 부족했다.
특히 공유 기능에서 개인별로 생성된 별자리 이미지가 직접 공유되지 않고 서비스 링크만 공유되는 점이 아쉬웠다. 사용자가 자신의 별자리를 SNS에 바로 공유할 수 있었다면 유입이 하나라도 늘지 않았을까 아쉬움이 남는다.
현재 공유 기능에서는 서비스의 기본 URL만 기본 클립보드에 복사되는데, 사용자의 별자리 정보를 ULR 파라미터나 상태로 저장하여 공유 시 해당 별자리가 보이도록 하는게 이상적이지 않았나 아쉽고, sns 플랫폼에 직접 공유할 수 있는 기능도 추가하면 좋았을 것 같다.
또 별자리 생성 과정에서 애니메이션 효과를 추가하고, 사용자가 별자리의 일부 요소를 직접 조작할 수 있는 기능을 구현했다면 더 몰입감 있었을텐데 여러모로 아쉬움이 많이 남는 부분이다.
2. 프로젝트 방향성
프로젝트 방향성에 대한 고민도 부족했던 것 같다. 처음 기획 의도는 "이름으로 심볼을 만들자"는 것이었는데 개발 과정에서 별자리 컨셉에 꽂히고, 새해 목표까지 추가되면서 이도저도 아닌 서비스가 탄생했다. 결과적으로는 "이름으로 별자리를 만드는 서비스"인지, "새해 목표를 꾸며주는 서비스"인지 나조차도 명확하게 정의내리기 어려운 상태가 되었다. 매일매일 생각해도 매일매일 모르겠는 서비스다..🧐🧐이게 제일 아쉬운 점이다.
이런 모호한 정체성이 사용자들에게도 그대로 전달되어 서비스의 목적을 제대로 인식하지 못하고 혼란스럽게 했을 것 같다. 처음부터 명확히 컨셉을 정하고, 일관되게 사용자 경험을 설계했다면 조금 더 강한 인상을 남길 수 있었을 것 같다. 앞으로는 프로젝트 시작 전 명확한 목표와 타겟 사용자를 설정하고, 일관된 방향성을 유지하는 것이 중요하다는 큰 교훈을 얻었다.
3. 디자인 측면
디자인에 대한 고민도 많았다. 타로 카드 느낌도 내고 싶어 완성한 디자인이지만 대중적으로 받아들여지는 디자인인지 고민이 많았다. 1번 패턴이 특히 굉장히 난해한 것 같기도 하고...배경에 성운처럼 흩어진 별을 표현하고자 했는데 먼지처럼 보이기도 한 것 같다. 내가 디자이너가 아니라 어찌할 수 없는 부분이긴 하다....내 보기엔 이쁘게 최선을 다함
😼마무리
@namespace는 기술적으로나 디자인적으로나 도전으로 가득찼던 프로젝트다. 완벽한 프로젝트는 아니지만, 개발 과정에서 어떤 프로젝트보다도 많은 고민과 성찰을 했고, 그만큼 성장할 수 있었던 것 같다.
키보드로 만드는 심볼-이 기믹은 많은 애정이 있어서 이번 프로젝트에서 아쉬웠던 점을 보완하여 더 발전한 서비스를 만들어보고 싶다. 애니메이션도 강화하고 많은 react-share 등 다양한 라이브러리를 사용해볼 생각
@namespace v2 coming soon💫
https://namespace-ten.vercel.app/
@NAMESPACE
namespace-ten.vercel.app