PHP 세션이 끊긴 상태에서 버튼을 누르면 권한이 없다는 메시지가 떴다. 화면은 그대로라 사용자는 계속 누르고 계속 같은 메시지가 뜬다.
Table of contents
Open Table of contents
알리기만 했다
응답 처리가 이랬다.
$.post(url, data, function(res) {
if (res.code !== 'OK') {
alert(res.message);
return;
}
...
});
alert 로 메시지를 띄우고 끝난다. 무엇을 해야 하는지가 없다.
res.code 가 무엇이든 같은 alert 이 뜬다. 세션이 끊긴 것이면 로그인을 해야 하는데 사용자는 그것을 모른다. 메시지 자체는 틀리지 않았지만 그것만으로는 다음에 무엇을 해야 하는지가 안 나온다.
오류를 알리는 것과 무엇을 하라고 알리는 것은 다른 일이다.
오류 유형을 나눴다
응답에 유형을 넣었다.
return $this->json([
'code' => 'UNAUTHORIZED',
'message' => '로그인이 필요합니다',
]);
http_response_code 로 상태 코드도 갈랐다. 인증이 끊긴 것은 401이고 자격이 없는 것은 403이다. 앞은 다시 로그인하면 풀리고 뒤는 안 풀린다.
if (res.code === 'UNAUTHORIZED') {
location.href = '/login?redirect=' + encodeURIComponent(location.pathname + location.search);
return;
}
if (res.code === 'FORBIDDEN') {
alert('이 작업을 할 권한이 없습니다. 관리자에게 문의하십시오.');
return;
}
if (res.code === 'VALIDATION') {
showFieldErrors(res.errors); // 어느 칸이 잘못됐는지 표시
return;
}
alert(res.message);
UNAUTHORIZED 면 로그인 화면으로 보낸다. location.pathname 을 넘겨서 로그인 뒤에 돌아온다.
돌아올 주소를 검사했다
redirect 값을 그대로 쓰면 위험했다. 외부 주소를 넣으면 로그인 뒤에 그리로 보낸다.
$redirect = $this->input->get('redirect');
if (!$redirect || parse_url($redirect, PHP_URL_HOST) !== null) {
$redirect = '/';
}
처음에는 슬래시로 시작하는지만 보면 되겠다고 생각했는데 그것으로는 안 걸러진다. 슬래시 두 개로 시작하는 주소는 앞의 규약을 생략한 형태라 브라우저가 바깥 호스트로 읽는다.
parse_url 에 넣어 보면 호스트 자리가 채워져 나오므로 그것이 잡히면 거절하게 했다.
검증 — 칸을 짚고 재시도를 안내
VALIDATION 에 메시지 하나만 담으면 어느 칸인지 모른다.
return $this->json([
'code' => 'VALIDATION',
'errors' => [
'email' => '메일 형식이 아닙니다',
'phone' => '숫자만 입력하십시오',
],
]);
showFieldErrors 로 해당 입력칸 아래에 표시했다.
function showFieldErrors(errors) {
$('.field-error').remove();
for (var name in errors) {
$('[name="' + name + '"]').after(
'<div class="field-error">' + errors[name] + '</div>');
}
$('[name="' + Object.keys(errors)[0] + '"]').focus();
}
첫 오류 칸에 focus 를 옮겼다. 긴 양식에서는 어디로 가야 할지 모른다.
오류를 세 부류로 나누기도 했다.
| 부류 | 사용자가 할 일 |
|---|---|
| 입력 오류 | 고쳐서 다시 |
| 권한 오류 | 로그인하거나 문의 |
| 시스템 오류 | 잠시 뒤 다시 |
SYSTEM 쪽은 다시 눌러 보면 될 수 있어서 그것을 알려 줬다.
일시적인 문제가 발생했습니다. 잠시 후 다시 시도해 주십시오.
(오류 번호: A20160612-1834)
$errorId = 'A' . date('Ymd') . '-' . substr(uniqid(), -4);
log_message('error', "[{$errorId}] " . $e->getMessage());
오류 번호를 같이 내면 문의가 왔을 때 로그에서 그 번호로 찾는다.
내부 정보와 문구 통합
처음에는 getMessage 가 준 예외 메시지를 그대로 보여 주고 있었다.
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'x' in 'where clause'
SQLSTATE 코드는 사용자에게 아무 의미가 없고 내부 구조를 드러낸다.
$msg = (ENVIRONMENT === 'production')
? '일시적인 문제가 발생했습니다'
: $e->getMessage();
운영에서는 일반 문구를 보이고 개발에서는 상세를 보이게 나눴다.
같은 상황의 문구가 화면마다 다른 것도 고쳤다. 로그인이 필요하다는 것과 세션이 만료되었다는 것과 다시 로그인해 달라는 것이 전부 같은 상황인데 표현이 갈려 있었다.
// config/messages.php
$config['msg']['unauthorized'] = '로그인이 필요합니다. 다시 로그인해 주십시오.';
$config['msg']['forbidden'] = '이 작업을 할 권한이 없습니다.';
messages.php 한 곳에 모으고 각 화면이 참조하게 하니 문구를 고칠 일이 있으면 한 곳만 고친다.
정리
- 오류를 알리는 것과 무엇을 하라고 알리는 것은 다르다
- 알리기만 하면 사용자가 같은 자리를 반복한다
- 응답에 오류 유형을 넣으면 유형별로 다르게 처리할 수 있다
- 인증 끊김과 자격 부족은 상태 코드를 다르게 준다
- 인증이 끊겼으면 로그인 화면으로 보내고 원래 주소를 넘긴다
- 돌아올 주소는
parse_url로 호스트가 잡히는지 본다 - 슬래시로 시작하는지만 보면 슬래시 두 개짜리가 통과한다
- 검증 오류는 어느 칸인지 짚고 첫 오류 칸에
focus를 옮긴다 - 다시 시도해도 되는 오류인지 알려 준다
- 오류 번호를 같이 내면 문의 추적이 된다
- 내부 예외 메시지를 사용자에게 그대로 보이지 않는다
- 같은 상황의 문구를 한 곳에 모은다