stack-analysis
CHAPTER 89 / 90
읽기 약 2분
FUNCTION
프로필/설정/개인정보
핵심 개념
프로필 편집·privacy·GDPR·계정 삭제·데이터 다운로드 — 사용자 컨트롤.
본문
프로필 모델
ALTER TABLE users ADD COLUMN bio TEXT;
ALTER TABLE users ADD COLUMN website VARCHAR;
ALTER TABLE users ADD COLUMN location VARCHAR;
ALTER TABLE users ADD COLUMN birthday DATE;
ALTER TABLE users ADD COLUMN gender VARCHAR;
ALTER TABLE users ADD COLUMN cover_url VARCHAR;
ALTER TABLE users ADD COLUMN privacy_settings JSONB DEFAULT '{
"profile_visibility": "public",
"email_visible": false,
"show_birthday": false,
"allow_messages": "everyone",
"indexable": true
}';프로필 편집
'use client';
const ProfileSchema = z.object({
name: z.string().min(1).max(50),
handle: z.string().regex(/^[a-z0-9_]{3,30}$/),
bio: z.string().max(160).optional(),
website: z.string().url().optional().or(z.literal('')),
location: z.string().max(50).optional(),
birthday: z.string().date().optional(),
});
function ProfileEditPage({ user }: { user: User }) {
const { register, handleSubmit, formState: { errors, isDirty, isSubmitting } } =
useForm<z.infer<typeof ProfileSchema>>({
resolver: zodResolver(ProfileSchema),
defaultValues: user,
});
const onSubmit = async (data: any) => {
const res = await fetch('/api/me/profile', {
method: 'PATCH',
body: JSON.stringify(data),
});
if (!res.ok) {
const err = await res.json();
if (err.code === 'handle_taken') {
toast.error('이미 사용 중인 사용자명');
}
return;
}
toast.success('프로필 업데이트됨');
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<AvatarUpload userId={user.id} currentUrl={user.avatarUrl} />
<CoverUpload userId={user.id} currentUrl={user.coverUrl} />
<Field label="이름" error={errors.name?.message}>
<input {...register('name')} />
</Field>
<Field label="사용자명" error={errors.handle?.message}>
<input {...register('handle')} />
<span className="text-sm">openhyperstep.com/@{watch('handle')}</span>
</Field>
<Field label="소개" error={errors.bio?.message}>
<textarea {...register('bio')} rows={3} />
<CharCount value={watch('bio') ?? ''} max={160} />
</Field>
<button disabled={!isDirty || isSubmitting}>
{isSubmitting ? '저장 중...' : '저장'}
</button>
</form>
);
}Privacy Settings
function PrivacySettings({ user }: { user: User }) {
const updateSetting = async (key: string, value: any) => {
await fetch('/api/me/privacy', {
method: 'PATCH',
body: JSON.stringify({ [key]: value }),
});
};
return (
<div className="space-y-6">
<Setting
label="프로필 공개 범위"
value={user.privacySettings.profile_visibility}
options={[
{ value: 'public', label: '공개' },
{ value: 'followers', label: '팔로워만' },
{ value: 'private', label: '비공개' },
]}
onChange={v => updateSetting('profile_visibility', v)}
/>
<Toggle
label="이메일 공개"
description="다른 사용자가 내 이메일을 볼 수 있습니다"
value={user.privacySettings.email_visible}
onChange={v => updateSetting('email_visible', v)}
/>
<Setting
label="DM 받기"
value={user.privacySettings.allow_messages}
options={[
{ value: 'everyone', label: '누구나' },
{ value: 'following', label: '팔로잉만' },
{ value: 'no_one', label: '받지 않음' },
]}
onChange={v => updateSetting('allow_messages', v)}
/>
<Toggle
label="검색엔진 인덱싱 허용"
description="Google에서 내 프로필이 검색되도록"
value={user.privacySettings.indexable}
onChange={v => updateSetting('indexable', v)}
/>
</div>
);
}데이터 다운로드 (GDPR)
// 사용자가 자신의 모든 데이터 요청
app.post('/api/me/data-export', authenticate, async (req, res) => {
const userId = req.user!.id;
// 비동기 처리 — 큐
await dataExportQueue.add('export', { userId });
res.json({
ok: true,
message: '데이터 준비 중. 24시간 내 이메일로 발송됩니다.',
});
});
const exportWorker = new Worker('data-export', async (job) => {
const { userId } = job.data;
const user = await db.user.findUnique({ where: { id: userId } });
// 모든 사용자 데이터 수집
const data = {
profile: user,
posts: await db.post.findMany({ where: { userId } }),
comments: await db.comment.findMany({ where: { userId } }),
likes: await db.like.findMany({ where: { userId } }),
messages: await db.message.findMany({ where: { senderId: userId } }),
follows: await db.follow.findMany({ where: { followerId: userId } }),
// ...
};
// ZIP 생성
const archive = createZip();
archive.append(JSON.stringify(data, null, 2), { name: 'data.json' });
// 미디어 파일도
for (const post of data.posts) {
if (post.mediaUrl) {
const file = await s3.send(new GetObjectCommand({ Bucket, Key: post.mediaUrl }));
archive.append(file.Body, { name: `media/${post.id}.jpg` });
}
}
// S3 업로드 (24시간 후 자동 삭제)
const exportKey = `exports/${userId}/${Date.now()}.zip`;
await s3.send(new PutObjectCommand({
Bucket, Key: exportKey, Body: archive, Tagging: 'expires=24h',
}));
// 이메일 발송 (presigned URL)
const url = await getSignedUrl(s3, new GetObjectCommand({ Bucket, Key: exportKey }), {
expiresIn: 24 * 3600,
});
await emailQueue.add('data-export-ready', { email: user!.email, url });
});계정 삭제 (Right to Erasure)
async function deleteAccount(userId: string, reason?: string) {
// 1. 즉시 비활성화
await db.user.update({
where: { id: userId },
data: {
status: 'deleted',
email: `deleted_${userId}@example.com`,
handle: `deleted_${userId}`,
name: '탈퇴한 사용자',
avatarUrl: null,
bio: null,
},
});
// 2. 비동기로 데이터 정리 (30일 그레이스)
await deletionQueue.add('purge', { userId, reason }, {
delay: 30 * 86400 * 1000,
});
}
const deletionWorker = new Worker('purge', async (job) => {
const { userId } = job.data;
// 작성한 콘텐츠 → 익명화 또는 삭제
await db.post.deleteMany({ where: { userId } });
await db.comment.updateMany({
where: { userId },
data: { content: '[삭제됨]', userId: ANONYMOUS_USER_ID },
});
// 미디어 파일 S3 삭제
const media = await db.media.findMany({ where: { userId } });
for (const m of media) {
await s3.send(new DeleteObjectCommand({ Bucket, Key: m.s3Key }));
}
// 계정 완전 삭제
await db.user.delete({ where: { id: userId } });
});다음 챕터
CH.90 "커뮤니티 종합".
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 프로젝트의 프로필 설정 개인정보 부분을 분석해서 실전 분석 + 개선 우선순위를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
프로필 설정 개인정보 관련 실제 서비스 5개를 비교 분석해서 패턴 추출를 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 코드베이스에서 프로필 설정 개인정보 최적화 가능 위치를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 풀스택 시장의 프로필 설정 개인정보 트렌드를 솔직히 알려줘.
⭐ 이것만 기억하세요
프로필/설정/개인정보는 이 3가지만 확실히 잡으세요
1.프로필 편집은 Zod 검증 + handle uniqueness — 사용자명 충돌 방지
2.Privacy Settings는 JSONB 컬럼 — 유연한 설정 추가
3.GDPR 준수 = 데이터 다운로드 + 30일 그레이스 후 완전 삭제
공유하기
진행도 89 / 90