ํค์ด, ํจ์ , ๋ฉ์ดํฌ์ , ํผ๋ถ ๋ถ์ผ์ ์ ๋ฌธ๊ฐ์ ๊ณ ๊ฐ์ ์ฐ๊ฒฐํ๋ ๋ทฐํฐ ์ปจ์คํ ์๋น์ค
Menual์ ๊ณ ๊ฐ์ด ๋จ๊ณ๋ณ ๊ณ ๋ฏผ์ง๋ฅผ ์์ฑํด ๋ทฐํฐ ์ ๋ฌธ๊ฐ์๊ฒ ์ ๋ฌํ๋ฉด, ์ ๋ฌธ๊ฐ๊ฐ ์๋ฃจ์ ์ง๋ก ๋ตํ๊ณ ์ค์๊ฐ ์ฑํ ์ผ๋ก ์ถ๊ฐ ์๋ด๊น์ง ์ด์ด์ง๋ ํ๋ซํผ์ ๋๋ค.
- ๊ฐ๋ฐ ๊ธฐ๊ฐ:
2025.09 - 2026.02
ํ ํ๋ก์ ํธ Menual ๋ด์์ ์ง์ ๊ตฌํํ ํํธ์ ํฌํธํด๋ฆฌ์ค ๋ฌธ์์ ๋๋ค.
GitHub Actions + Vercel ๊ธฐ๋ฐ ์๋ ๋ฐฐํฌ ํ์ดํ๋ผ์ธ ๊ตฌ์ฑ
develop ๋ธ๋์น์ Push๊ฐ ๋ฐ์ํ๋ฉด ๋ณ๋์ ์กฐ์ ์์ด ํ๋ก๋์
๊น์ง ์๋์ผ๋ก ๋ฐฐํฌ๋ฉ๋๋ค.
- GitHub Actions์์ ๋น๋ ํ
cpina/github-action-push-to-another-repository๋ก ๋น๋ ๊ฒฐ๊ณผ๋ฌผ์ ๋ฐฐํฌ ์ ์ฉ ๋ ํฌ์งํ ๋ฆฌ์ ์๋ Push - Vercel์ด ๋ฐฐํฌ ๋ ํฌ๋ฅผ ๊ฐ์งํด ์ฆ์ ํ๋ก๋์ ๋ฐ์
- ์ปค๋ฐ ๋ฉ์์ง๋ฅผ ๋ฐฐํฌ ๋ ํฌ์ ์ปค๋ฐ์ ๊ทธ๋๋ก ์ ๋ฌํด ๋ฐฐํฌ ์ด๋ ฅ ์ถ์ ๊ฐ๋ฅ
# .github/workflows/deploy.yml
- name: Pushes to another repository
uses: cpina/github-action-push-to-another-repository@main
env:
API_TOKEN_GITHUB: ${{ secrets.AUTO_ACTIONS }}
with:
source-directory: "output"
destination-github-username: dragunshin
destination-repository-name: ge-fe
commit-message: ${{ github.event.commits[0].message }}
target-branch: developSTOMP over SockJS ๊ธฐ๋ฐ WebSocket ์ค์๊ฐ ์ฑํ ์์คํ
์ฌ๋ฌ ์ปดํฌ๋ํธ๊ฐ ๋์์ ๋ง์ดํธ๋์ด๋ WebSocket ์ฐ๊ฒฐ์ด ๋จ ํ๋๋ง ์ ์ง๋๋๋ก ๋ชจ๋ ๋ ๋ฒจ ์ฑ๊ธํค ํจํด์ ์ ์ฉํ์ต๋๋ค.
- ๋ง์ดํธ ์นด์ดํฐ(
mounts)๋ก ์ฐ๊ฒฐ ์๋ช ์ฃผ๊ธฐ ๊ด๋ฆฌ โ ๋ชจ๋ ์ปดํฌ๋ํธ ์ธ๋ง์ดํธ ์์๋ง ์ฐ๊ฒฐ ํด์ onConnectํ๋ ์์user-nameํค๋๋ฅผ ํ์ฑํด ๋ณธ์ธuserId๋ฅผ ์ถ์ถ โ ๋ฉ์์ง ๋งํ์ ์ข์ฐ ๊ตฌ๋ถ์ ํ์ฉ- ์ฌ์ฐ๊ฒฐ ์
onConnect์์ ๋ฐ๋ฆฐ ๊ตฌ๋ ์ ์๋ flush โ ๋คํธ์ํฌ ๋ณต๊ตฌ ํ ๋ฉ์์ง ์์ ์ฆ์ ์ฌ๊ฐ ws:///wss://์คํด์http:///https://๋ก ์๋ ๋ณํ (SockJS ์๊ตฌ์ฌํญ ์ฒ๋ฆฌ)
// onConnect์์ ๋ณธ์ธ userId ํ์ฑ
onConnect: (frame: IFrame) => {
emitReady(true);
const myId = parseMyUserIdFromHeaders(frame.headers as Record<string, string>);
if (myId != null) emitMyUserId(myId);
// ์ฌ์ฐ๊ฒฐ ์ ๋ฐ๋ฆฐ ๊ตฌ๋
flush
for (const r of subs) {
if (!r.active) continue;
if (!r.sub) r.sub = client.subscribe(r.destination, r.handler);
}
},- ํ์คํ ๋ฆฌ ๋ก๋ ์คํจ(403)๊ฐ ์ค์๊ฐ ์ฐ๊ฒฐ์ ๋ง์ง ์๋๋ก ๋
๋ฆฝ์ ์ธ
useEffect๋ก ๋ถ๋ฆฌ roomIdRef๋ก ์ฝ๋ฐฑ ๋ด stale ํด๋ก์ ๋ฐฉ์ง โ ๋ฃธ ID ๋ณ๊ฒฝ ์์๋ ์์ ํ๊ฒ ๊ตฌ๋functional update+messageId์ค๋ณต ์ฒดํฌ๋ก ์๋ฒ ์์ฝ ๋ฉ์์ง ์ค๋ณต ๋ ๋๋ง ๋ฐฉ์ง
// functional update๋ก stale ํด๋ก์ ์์ด ์ค๋ณต ๋ฉ์์ง ์ ๊ฑฐ
setMessages((prev) => {
if (prev.some((m) => m.messageId === incoming.messageId)) return prev;
return [...prev, incoming];
});URL ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ๊ธฐ๋ฐ 7๋จ๊ณ ๋ฉํฐ ์คํ ํผ
ํค์ด ์๋ด์ ํ์ํ ์ ๋ณด(์ฌ์ง 4์ข + ์ผ๊ตดํ + ์ํ๋ ์ด๋ฏธ์ง + ์ง๋ฌธ)๋ฅผ ๋จ๊ณ๋ณ๋ก ์์งํฉ๋๋ค.
| ๋จ๊ณ | ๋ด์ฉ |
|---|---|
| 1/7 | ํ์์ ํค์ด์คํ์ผ ์ฌ์ง ์ ๋ก๋ |
| 2/7 | ๋จธ๋ฆฌ๋ฅผ ์ฌ๋ฆฐ ์ ๋ฉด ์ฌ์ง ์ ๋ก๋ |
| 3/7 | ์ธก๋ฉด ์ฌ์ง ์ ๋ก๋ |
| 4/7 | ์ํ๋ ์ด๋ฏธ์ง ์ฌ์ง ์ ๋ก๋ |
| 5/7 | ์ผ๊ตดํ ์ ํ |
| 6/7 | ์ํ๋ ํค์ด ์คํ์ผ ์ ํ |
| 7/7 | ์ถ๊ฐ ์ง๋ฌธ ์์ฑ |
?step=N&reservationId=X์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ก ๋จ๊ณ ๊ด๋ฆฌ โ ์๋ก๊ณ ์นจ, ๊ณต์ URL์๋ ์ํ ์ ์งuseLayoutEffect+requestAnimationFrame์ผ๋ก ๋จ๊ณ ์ ํ๋ง๋ค ์คํฌ๋กค์ด ์๋จ์ผ๋ก ์ด๋useHairSetupStore(Zustand)๋ก ๋จ๊ณ ๊ฐ ์ฌ์ง ํค(S3 ๊ฒฝ๋ก) ๋ณด์กด- S3 Presigned URL๋ก ์ฌ์ง์ ์ง์ ์ ๋ก๋ โ ์๋ฒ ๋ถํ ์์ด ๋์ฉ๋ ์ด๋ฏธ์ง ์ฒ๋ฆฌ
// ๋จ๊ณ ์ ํ ์ ์คํฌ๋กค ํ ๋ณด์ฅ
useLayoutEffect(() => {
requestAnimationFrame(() => {
window.scrollTo({ top: 0, left: 0, behavior: "auto" });
(document.scrollingElement ?? document.documentElement).scrollTop = 0;
});
}, [loc.key, step]);๋ฌธ์ ์ํฉ: ์๋ฒ ์ค๋ฅ ์ ํ๋ฆฌ๋ทฐ๊ฐ ๋จ์ "์ ๋ก๋๋ ๊ฒ์ฒ๋ผ" ๋ณด์ด๋ UX ์ค๋ฅ
์ฌ์ง์ ์ ํํ๋ฉด ๋ก์ปฌ Object URL๋ก ํ๋ฆฌ๋ทฐ๋ฅผ ์ฆ์ ๋ณด์ฌ์ค๋๋ค. ๊ทธ๋ฐ๋ฐ ์ดํ S3 ์
๋ก๋๊ฐ ์คํจํ๋ฉด, ํ๋ฆฌ๋ทฐ๋ ๊ทธ๋๋ก ๋ณด์ด๋ ์ฑ S3 ํค๋ง ์๋ ์ํ๊ฐ ๋ฉ๋๋ค. ์ฌ์ฉ์๋ ์
๋ก๋๊ฐ ๋ ์ค ์๊ณ ๋ค์ ๋จ๊ณ๋ก ๋์ด๊ฐ์ง๋ง ์ค์ ๋ก๋ ์ด๋ฏธ์ง๊ฐ ์ ์ฅ๋์ง ์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.
๋จ์ผ ์ฌ์ง ์
๋ก๋ (SinglePhotoField)
- ํ์ผ ์ ํ ์ฆ์
URL.createObjectURL()๋ก ๋ก์ปฌ ํ๋ฆฌ๋ทฐ๋ฅผ ํ์ํ๊ณ S3 ์ ๋ก๋ ์์ AbortController๋ฅผabortRef๋ก ๋ณด๊ด โ ์ ์ ๊ฐ X ๋ฒํผ์ ๋๋ฅด๊ฑฐ๋, ์ ํ์ผ์ ์ ํํ๊ฑฐ๋, ์ปดํฌ๋ํธ๊ฐ ์ธ๋ง์ดํธ๋ ๋abort()ํธ์ถ๋ก ์งํ ์ค์ธ presign-PUT ์์ฒญ์ ์ฆ์ ์ทจ์seqRef(์ ๋ก๋ ์์ ์นด์ดํฐ)๋ก ์ ์ ๋ก๋๊ฐ ์์๋ ๋ค ๋์ฐฉํ ์ด์ ์๋ต์ ๋ฌด์ โ ๋น ๋ฅด๊ฒ ์ฌ์ง์ ๋ฐ๊ฟ๋ stale ๊ฒฐ๊ณผ๊ฐ Zustand์ ์ ์ฅ๋์ง ์์- S3 ์
๋ก๋ ์คํจ ์:
URL.revokeObjectURL()๋ก ํ๋ฆฌ๋ทฐ๋ฅผ ์ฆ์ ์ ๊ฑฐ โ ์ฌ์ฉ์๊ฐ ์ ๋ก๋๊ฐ ์๋ฃ๋ ๊ฒ์ผ๋ก ์ค์ธํ๋ ์ํฉ ๋ฐฉ์ง - presign ์์ฒญ๊ณผ S3 PUT ์์ฒญ ๋ชจ๋ ๋์ผํ
signal์ ์ ๋ฌํด ์ค๋จ ์ ๋คํธ์ํฌ ์์ฒญ์ด ๋ ๋จ๊ณ ๋ชจ๋ ์ทจ์๋จ
const onChange = async (file: File) => {
handleRemove(); // ์ด์ ์
๋ก๋ abort + ํ๋ฆฌ๋ทฐ ์ด๊ธฐํ
const localUrl = URL.createObjectURL(file);
setPreviewUrl(localUrl); // ๋ก์ปฌ ํ๋ฆฌ๋ทฐ ์ฆ์ ํ์
const controller = new AbortController();
abortRef.current = controller;
const mySeq = ++seqRef.current;
try {
const { key } = await uploadImageViaPresign({
file,
resourceType,
resourceId,
imageType,
signal: controller.signal, // presign + PUT ๋ชจ๋ ๋์ผ signal ์ ๋ฌ
});
if (seqRef.current !== mySeq) return; // ์ ์
๋ก๋๊ฐ ์์๋์ผ๋ฉด ๊ฒฐ๊ณผ ๋ฌด์
onUploadedKey(key); // ์ฑ๊ณต ์์๋ง Zustand์ S3 ํค ์ ์ฅ
} catch (e) {
if (controller.signal.aborted) return; // abort๋ก ์ธํ ์๋ฌ๋ ๋ฌด์
clearLocalPreview(); // ์คํจ ์ ํ๋ฆฌ๋ทฐ ์ ๊ฑฐ โ ์คํด ๋ฐฉ์ง
} finally {
if (seqRef.current === mySeq) setIsUploading(false);
}
};๋ค์ค ์ฌ์ง ์
๋ก๋ (MultiPhotoPicker)
- ์์ฑํ ๋ชจ๋
Object URL์objectUrlsRef(Set)์ ๋ฑ๋กํด ๋๋ฝ ์์ด ์ถ์ - ์
๋ก๋ ์คํจ ์ ํด๋น
Object URL๋ง ์ฆ์ revoke + Set์์ ์ ๊ฑฐ โ ํ๋ฆฌ๋ทฐ ์ฌ๋ผ์ง - ์ปดํฌ๋ํธ ์ธ๋ง์ดํธ ์ Set์ ๋จ์ ๋ชจ๋ URL์ ์ผ๊ด revoke โ ๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง
useEffect(() => {
return () => {
objectUrlsRef.current.forEach((url) => URL.revokeObjectURL(url));
objectUrlsRef.current.clear();
};
}, []);
const addFile = async (file: File) => {
const localUrl = URL.createObjectURL(file);
objectUrlsRef.current.add(localUrl);
try {
const { key } = await uploadImageViaPresign({ file, ... });
setPreviews((p) => [...p, { key, previewUrl: localUrl }]); // ์ฑ๊ณต ์์๋ง ํ๋ฆฌ๋ทฐ ๋ฑ๋ก
} catch {
URL.revokeObjectURL(localUrl);
objectUrlsRef.current.delete(localUrl); // ์คํจ ์ ํ๋ฆฌ๋ทฐ ์ฆ์ ์ ๊ฑฐ
}
};React Quill ๊ธฐ๋ฐ ์ ๋ฌธ๊ฐ ์๋ฃจ์ ์์ฑ ์๋ํฐ
- ํค์ด / ํจ์
์นดํ
๊ณ ๋ฆฌ๋ณ ๊ธฐ๋ณธ ํ
ํ๋ฆฟ HTML์ Quill
value๋ก ์ฃผ์ ํด ์ ๋ฌธ๊ฐ๊ฐ ํญ๋ชฉ๋ณ๋ก ๋ฐ๋ก ์์ฑ ๊ฐ๋ฅ - ์ด๋ฏธ์ง ์ฝ์ ์ S3 Presigned URL๋ก ์ง์ ์ ๋ก๋ ํ ์ด๋ฏธ์ง URL์ Quill์ ์ฝ์
- ๊ณ ๊ฐ์ ๊ณ ๋ฏผ์ง(์ค๋ฌธ ์๋ต)๋ฅผ ์๋จ์ ํจ๊ป ๋ ๋๋งํด ์ ๋ฌธ๊ฐ๊ฐ ์ปจํ ์คํธ๋ฅผ ๋ณด๋ฉฐ ์์ฑ ๊ฐ๋ฅ
- ๊ณ ๊ฐ์
/consultations/:consultationId/solution์์ ์๋ฃจ์ ์ง ์ด๋ ๋ฐ ๋ด ์๋ฃจ์ ๋ชฉ๋ก ์กฐํ
์ผ๋ฐ ์ฌ์ฉ์ / ์ ๋ฌธ๊ฐ ์ญํ ๋ณ๋ก ๋ถ๊ธฐ๋๋ ๋ง์ดํ์ด์ง
| ํ์ด์ง | ๋ด์ฉ |
|---|---|
| ๋ง์ดํ์ด์ง ๋ฉ์ธ | ์ ์ ํ์ ๊ฐ์ง ํ ์ผ๋ฐ / ์ ๋ฌธ๊ฐ ๋ฉ๋ด ๋ถ๊ธฐ ๋ ๋๋ง |
| ์ฐ ๋ชฉ๋ก | ์ข์์ํ ์ ๋ฌธ๊ฐ ๋ชฉ๋ก ์กฐํ |
| ํฌ์ธํธ ๋ด์ญ | ํฌ์ธํธ ์ ๋ฆฝ / ์ฌ์ฉ ๋ด์ญ |
| ๊ฒฐ์ ๋ด์ญ | ๊ฒฐ์ ๋ด์ญ ๋ชฉ๋ก |
| ์์ฝ ๋ด์ญ | ์์ฝ ์ํ๋ณ ๋ชฉ๋ก ์กฐํ |
| ๋ด ๋ฆฌ๋ทฐ | ์์ฑํ ๋ฆฌ๋ทฐ ๋ชฉ๋ก ์กฐํ |
| ๋ฆฌ๋ทฐ ์์ฑ | ์๋ด ์๋ฃ ๊ฑด์ ๋ํ ๋ณ์ + ํ ์คํธ ๋ฆฌ๋ทฐ ์์ฑ |
| ์ ๋ฌธ๊ฐ ์๊ธฐ์๊ฐ | React Quill๋ก ์๊ธฐ์๊ฐ ํธ์ง |
| ์ ๋ฌธ๊ฐ ํฌํธํด๋ฆฌ์ค | ํฌํธํด๋ฆฌ์ค ๋ชฉ๋ก ๊ด๋ฆฌ (์ 4๋ฒ ์ฐธ๊ณ ) |
| ์ ๋ฌธ๊ฐ ์ผ์ ์ค์ | ์๋ด ์ ํ / ๊ฐ๊ฒฉ ์ค์ (์ 4๋ฒ ์ฐธ๊ณ ) |
| ์ ๋ฌธ๊ฐ ์๋ด ๋ด์ญ | ์งํํ ์๋ด ์ด๋ ฅ ์กฐํ |
| ๋ฌธ์ | ํด๊ฒฐ ๋ฐฉ๋ฒ |
|---|---|
| ์ฌ๋ฌ ์ปดํฌ๋ํธ์์ ๋์์ WebSocket์ ๋ง์ดํธํ๋ฉด ์ค๋ณต ์ฐ๊ฒฐ ๋ฐ์ | ๋ชจ๋ ๋ ๋ฒจ ์ฑ๊ธํค + ๋ง์ดํธ ์นด์ดํฐ๋ก ์ฐ๊ฒฐ 1๊ฐ ์ ์ง |
| ์ฌ์ฐ๊ฒฐ ํ ๊ตฌ๋ ์ด ์ฌ๋ผ์ ธ ๋ฉ์์ง ๋ฏธ์์ | onConnect์์ ํ์ฑ ๊ตฌ๋
๋ชฉ๋ก์ flushํด ์๋ ์ฌ๊ตฌ๋
|
์์ผ ์ฝ๋ฐฑ ๋ด roomId๊ฐ stale ํด๋ก์ ๋ก ์ด์ ๊ฐ ์ฐธ์กฐ |
useRef๋ก ์ต์ ๊ฐ ๋๊ธฐํ, functional update๋ก prev ์ฌ์ฉ |
| ์๋ฒ ์์ฝ ๋ฉ์์ง๋ก ์ธํ ๋ฉ์์ง ์ค๋ณต ๋ ๋๋ง | messageId ๊ธฐ๋ฐ ์ค๋ณต ์ฒดํฌ |
| ๋จ๊ณ ์ ํ ์ ์คํฌ๋กค ์์น๊ฐ ์ด์ ๋จ๊ณ์ ์์น์ ๋จธ๋ฌด๋ฆ | useLayoutEffect + requestAnimationFrame์ผ๋ก ๋ ๋ ํ ์คํฌ๋กค ํ ๋ณด์ฅ |
| S3 ์ ๋ก๋ ์คํจ ์ ํ๋ฆฌ๋ทฐ๊ฐ ๋จ์ ์ ๋ก๋ ์๋ฃ๋ก ์ค์ธ | ์
๋ก๋ ์คํจ ์ฆ์ URL.revokeObjectURL()๋ก ํ๋ฆฌ๋ทฐ ์ ๊ฑฐ, S3 ํค๋ ์ฑ๊ณต ์์๋ง Zustand์ ์ ์ฅ |
| ์ฌ์ง์ ๋น ๋ฅด๊ฒ ๊ต์ฒดํ๋ฉด ์ด์ ์ ๋ก๋ ๊ฒฐ๊ณผ๊ฐ ๋ฆ๊ฒ ๋์ฐฉํด ์๋ชป๋ ํค ์ ์ฅ | seqRef ์นด์ดํฐ๋ก stale ์๋ต ๊ฐ์ง ํ ๋ฌด์, AbortController๋ก ์ด์ ์์ฒญ ์ฆ์ ์ทจ์ |
| ์ปดํฌ๋ํธ ์ธ๋ง์ดํธ ์ Object URL์ด ๋ฉ๋ชจ๋ฆฌ์ ๋์ | objectUrlsRef(Set)๋ก ์์ฑํ ๋ชจ๋ URL ์ถ์ โ ์ธ๋ง์ดํธ ์ ์ผ๊ด revoke |