설정·환경설정 UX: 사용자가 직접 제어하는 앱 경험 설계

UX 디자인

설정 UX환경설정사용자 제어앱 설정토글 설계

이 글은 누구를 위한 것인가

  • 설정 페이지가 너무 복잡해서 사용자가 원하는 설정을 못 찾는 팀
  • "저장" 버튼 없이 실시간으로 설정을 반영해야 하는지 고민하는 팀
  • 설정 UX를 처음 구조화하는 프로덕트 디자이너

들어가며

설정 페이지는 "고급 사용자"가 사용하는 공간이다. 하지만 잘못 설계하면 기본 사용자도 헷갈린다. 계층 구조, 즉시 저장 여부, 설정 충돌 처리가 핵심이다. 항목이 많아질 때의 노출 전략은 점진적 공개 원칙을 따른다.

이 글은 bluefoxdev.kr의 설정 UX 가이드 를 참고하여 작성했습니다.


1. 설정 계층 구조 설계

[설정 분류 기준]

계정 설정:
  프로필 (이름, 사진, 이메일)
  비밀번호 & 보안 (2FA, 세션)
  연결된 계정 (소셜 로그인)

알림 설정:
  채널별 (이메일/SMS/푸시)
  유형별 (주문/마케팅/시스템)
  시간대 설정

앱 환경설정:
  언어 및 지역
  테마 (라이트/다크/시스템)
  접근성

개인정보 & 데이터:
  데이터 다운로드
  계정 삭제
  쿠키 설정

[설정 저장 방식 선택]

즉시 저장 (토글, 드롭다운):
  변경 즉시 반영 → 저장 버튼 불필요
  ✅ 알림 On/Off
  ✅ 테마 전환
  ✅ 언어 변경

저장 버튼 필요 (폼):
  여러 필드를 한번에 저장
  ✅ 프로필 정보 (이름, 주소)
  ✅ 배송지 추가

[설정 완료 피드백]
  즉시 저장: "저장됨" 토스트 (간결하게)
  폼 저장: 인라인 성공 메시지 또는 토스트
  오류: 인라인 오류 + 스크롤 오류 위치로 이동

2. 설정 페이지 컴포넌트

interface SettingToggleProps {
  label: string;
  description?: string;
  value: boolean;
  onChange: (value: boolean) => void;
  disabled?: boolean;
}

function SettingToggle({ label, description, value, onChange, disabled }: SettingToggleProps) {
  const id = `toggle-${label.replace(/\s/g, "-").toLowerCase()}`;

  return (
    <div className="flex items-start justify-between py-4 border-b last:border-b-0">
      <div className="flex-1 mr-4">
        <label htmlFor={id} className="text-sm font-medium text-gray-900 cursor-pointer">
          {label}
        </label>
        {description && (
          <p className="text-xs text-gray-500 mt-0.5">{description}</p>
        )}
      </div>
      <button
        id={id}
        role="switch"
        aria-checked={value}
        onClick={() => !disabled && onChange(!value)}
        disabled={disabled}
        className={`relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ${
          value ? "bg-blue-600" : "bg-gray-200"
        } ${disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
      >
        <span
          className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ${
            value ? "translate-x-5" : "translate-x-0"
          }`}
        />
      </button>
    </div>
  );
}

// 알림 설정 섹션
function NotificationSettings() {
  const [settings, setSettings] = useState({
    emailOrders: true,
    emailMarketing: false,
    pushOrders: true,
    pushMarketing: true,
    smsOrders: false,
  });
  const { addToast } = useToast();

  const updateSetting = async (key: keyof typeof settings, value: boolean) => {
    const prev = settings[key];
    setSettings((s) => ({ ...s, [key]: value })); // 낙관적 업데이트
    
    try {
      await updateNotificationSetting(key, value);
      addToast({ type: "success", message: "알림 설정이 저장되었습니다" });
    } catch {
      setSettings((s) => ({ ...s, [key]: prev })); // 롤백
      addToast({ type: "error", message: "저장에 실패했습니다" });
    }
  };

  return (
    <section>
      <h2 className="text-lg font-semibold mb-1">알림 설정</h2>
      <p className="text-sm text-gray-500 mb-6">수신할 알림 종류를 선택하세요</p>
      
      <div className="bg-white rounded-xl border overflow-hidden">
        <div className="px-4 py-3 bg-gray-50 border-b">
          <h3 className="text-sm font-medium text-gray-700">이메일 알림</h3>
        </div>
        <div className="px-4">
          <SettingToggle
            label="주문 알림"
            description="주문 상태 변경 시 이메일로 알림"
            value={settings.emailOrders}
            onChange={(v) => updateSetting("emailOrders", v)}
          />
          <SettingToggle
            label="마케팅 이메일"
            description="새 상품, 할인, 이벤트 소식"
            value={settings.emailMarketing}
            onChange={(v) => updateSetting("emailMarketing", v)}
          />
        </div>
      </div>
    </section>
  );
}

마무리

설정 UX에서 가장 중요한 것은 "변경 사항이 즉시 반영되는가"다. 토글을 바꿨는데 저장 버튼을 눌러야 적용된다면 사용자는 혼란스럽다. 토글·드롭다운은 즉시 저장, 복잡한 폼은 저장 버튼으로 일관성 있게 구분하라.