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