우수사례 - Chrome에서 드래그 앤 드롭 다운로드

소개

드래그 앤 드롭 (DnD)은 HTML 5의 여러 가지 훌륭한 기능 중 하나이며 Firefox 3.5, Safari, Chrome, IE에서 지원됩니다. Google은 최근 Google Chrome 사용자가 브라우저에서 데스크톱으로 파일을 드래그 앤 드롭할 수 있는 새로운 기능을 출시했습니다. 매우 편리한 기능이지만 이 새로운 기능에 대한 역엔지니어링의 발견에 관한 기사를 라이언 세든이 게시하기 전까지는 널리 알려지지 않았습니다.

Box.net은 이러한 새로운 기능을 통해 클라우드 콘텐츠 관리 솔루션을 개선하고 개발자 커뮤니티에 더 많은 기여를 할 수 있게 되어 기쁩니다. DnD Download가 제품에 통합되었다는 소식을 전해드립니다. 이제 Box 사용자는 Chrome 브라우저에서 데스크톱으로 직접 파일을 드래그하여 파일을 다운로드하고 저장할 수 있습니다.

이 새로운 기능을 개발하는 동안 여러 번 반복한 과정을 공유하고자 합니다.

Drag and Drop API 지원 확인

먼저 브라우저에서 HTML5 드래그 앤 드롭을 완전히 지원하는지 확인합니다. 이를 실행하는 간단한 방법은 Modernizr라는 라이브러리를 사용하여 특정 기능을 확인하는 것입니다.

if (Modernizr.draganddrop) {
// Browser supports native HTML5 DnD.
} else {
// Fallback to a library solution.
}

반복 1

먼저 세드돈이 Gmail에서 찾은 접근 방식을 시도해 보았습니다. 파일의 앵커 링크에 'data-downloadurl'이라는 새 속성을 추가했습니다. 이 프로세스는 HTML5의 맞춤 데이터 속성을 사용합니다. data-downloadurl에는 파일의 MIME 유형, 대상 파일 이름(다운로드된 파일의 원하는 파일 이름), 파일의 다운로드 URL을 포함해야 합니다. 따라서 다음과 같이 HTML 템플릿에 추가됩니다.

<a href="#" class="dnd"
data-downloadurl="{$item.mime}:{$item.filename}:{$item.url}"></a>

다음과 같은 출력이 생성됩니다.

<a href="#" class="dnd" data-downloadurl=
"image/jpeg:Penguins.jpg:https://www.box.net/box_download_file?file_id=f66690"></a>

Seddon의 도움말을 바탕으로 폰 쇼르시가 만든 jQuery plugin을 기반으로 브라우저 기능 감지를 실행하는 jQuery 플러그인을 추가했습니다. 폰 쇼르슈 버전에 추가한 줄이 강조 표시되어 있습니다.

(function($) {

$.fn.extend({
dragout: function() {
var files = this;
if (files.length > 0) {
    $(files).each(function() {
    var url = (this.dataset && this.dataset.downloadurl) ||
                this.getAttribute("data-downloadurl");
    if (this.addEventListener) {
        this.addEventListener("dragstart", function(e) {
        if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
            e.dataTransfer.setData('DownloadURL', 'http://www.box.net')) {
            e.dataTransfer.setData("DownloadURL", url);
        }
        },false);
    }
    });
}
}
});

})(jQuery);

이렇게 한 이유는 이전 브라우저 감지 없이 IE에서 HTML 요소에 addEventListener()를 실행하면 IE에서 자체 attachEvent() 메서드를 사용하므로 JavaScript 오류가 발생하기 때문입니다. 현재 IE에서는 e.dataTransfer가 정의되지 않으며, Firefox(Mozilla)에서는 e.dataTransfer.constructor가 DataTransfer를 반환하고, Webkit 브라우저(Chrome 및 Safari)에서는 Clipboard 생성자를 구현합니다. Safari에서는 e.dataTransfer.setData('DownloadURL','http://www.box.net')가 false를 반환하고 Chrome에서는 이 문에 대해 true를 반환합니다. 위에 언급된 모든 테스트를 실행하면 이 기능을 Chrome에서만 사용할 수 있습니다. 다음과 같이 하면 된다고 주장할 수 있습니다.

/chrome/.test( navigator.userAgent.toLowerCase() )

하지만 DnD 다운로드가 작동하는지 기술적으로 감지하지 못하는 기능 감지를 브라우저 감지보다 선호합니다.

반복 1의 문제

1) 현재 폴더 간에 파일을 이동/복사하기 위해 페이지 내 DnD가 사용 설정되어 있으므로 DnD 다운로드와 페이지 내 DnD를 구분할 방법이 필요합니다. 기술적으로는 이 두 작업을 결합할 수 없습니다. 사용자가 Box.net 계정 내 다른 폴더로 파일을 이동할지 아니면 데스크톱으로 드래그할지 예측할 수 없습니다. 이 두 작업은 완전히 다릅니다. 또한 커서가 브라우저 창 외부에 있는지 쉽게 감지할 방법이 없습니다. window.onmouseout (IE) 및 document.onmouseout (기타 브라우저)를 사용하여 mouseout 이벤트를 문서에 연결하고 e.relatedTarget.nodeName == "HTML" (e는 사용 가능한 mouseout 이벤트 또는 window.event 중 어느 쪽이든)를 확인할 수 있습니다. 하지만 이벤트 버블링으로 인해 이는 매우 어렵습니다. 특히 Box.net과 같은 복잡한 웹 앱에서 이미지나 레이어 위에 있을 때 이벤트가 무작위로 트리거될 수 있습니다.

2) 사용자가 실수로 무언가를 데스크톱으로 드래그하지 않도록 하려면 사용자가 명시적으로 조치를 취해야 합니다. Box 폴더의 편집자가 다운로드하는 모든 사용자의 컴퓨터에서 원치 않는 작업을 실행하는 실행 파일을 업로드할 수 있습니다. 사용자는 파일이 언제 데스크톱에 다운로드되는지 정확하게 알고 싶어 합니다.

반복 2

Ctrl + 드래그 (Windows Ctrl 키를 누른 상태에서 파일을 드래그)를 실험해 보기로 했습니다. 이 작업은 사용자가 Windows 데스크톱에서 파일을 복제하기 위해 할 수 있는 작업과 일치합니다. 또한 실수로 파일이 다운로드되지 않도록 하려면 사용자의 추가 작업 (추가 단계는 아님)이 필요합니다.

DnD 오프라인 저장을 페이지 내 DnD와 긴밀하게 통합해야 하므로 이제 반복 1의 jQuery 플러그인은 지원 중단되었습니다. 관심이 있는 경우 jQuery UI의 Draggable 플러그인의 수정된 버전을 사용합니다. 타겟 요소의 mousedown 이벤트 내에 다음 코드를 배치합니다.

// DnD to desktop when the Ctrl key is pressed while dragging
if (e.ctrlKey) {
var that = $(e.target);
// make sure it is not IE (attachEvent).
if (that[0].addEventListener) {
    that[0].addEventListener("dragstart",function(e) {
        // e.dataTransfer in Firefox uses the DataTransfer constructor
        // instead of Clipboard
        // make sure it's Chrome and not Safari (both webkit-based).
        // setData on DownloadURL returns true on Chrome, and false on Safari
        if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
            e.dataTransfer.setData('DownloadURL','http://www.box.net')) {
        var url = (this.dataset && this.dataset.downloadurl) ||
                    this.getAttribute("data-downloadurl");
        e.dataTransfer.setData("DownloadURL", url);
        }
    }, false);
    return;
}
}

Ctrl 키를 사용 설정하는 것 외에도 사용자가 일반 페이지 내 드래그를 실행할 때 표시되는 작은 토스터 도움말을 추가했습니다. Ctrl 키를 누른 상태에서 파일 아이콘을 데스크톱으로 드래그하면 파일을 다운로드할 수 있다고 사용자에게 알려줍니다.

반복 2의 문제

보안 문제로 인해 Box.net은 정적 파일에 직접 액세스할 수 있는 영구 URL을 노출하지 않습니다. 이는 Box.net에만 해당하는 문제가 아닙니다. 모든 온라인 저장소 서비스는 파일이 공개 상태인지, 적절한 권한을 가진 사용자가 의도한 다운로드를 요청했는지 확인하는 추가 보안 레이어 없이 영구 URL을 노출해서는 안 됩니다.

항목의 '다운로드 URL' (예: https://www.box.net/box_download_file?file_id=f_60466690)을 따라가면 '302 찾음' 상태 코드를 반환하고 파일의 임시 '실제 URL'인 임의의 URL(예: https://www.box.net/dl/6045?a=1f1207a084&m=168299,11211&t=2&b=aca15820d924e3b)로 리디렉션됩니다. 문제는 몇 분마다 만료되므로 HTML 출력에 배치하는 것이 실용적이지 않다는 점입니다. 사용자가 몇 분 전에 생성된 HTML 출력의 링크에서 파일을 다운로드하려고 하면 '404'가 반환될 수 있습니다.

DnD 다운로드는 리소스를 직접 가리키는 실제 URL에서만 작동합니다. 리디렉션이 포함된 경우 현재 체인을 따르기에 충분히 스마트하지 않으며 보안상의 이유로 체인을 따라가서는 안 됩니다. 따라서 위의 링크 https://www.box.net/box_download_file?file_id=f_60466690를 브라우저 위치 표시줄에 입력하면 파일을 다운로드할 수 있지만 DnD로는 작동하지 않습니다.

'실제 URL'과 '리디렉션 URL'의 차이를 더 잘 이해하려면 스크린샷을 참고하세요.

302 리디렉션 URL
302 리디렉션 URL
실제 URL
실제 URL

반복 3

Ajax를 사용해 보겠습니다.

이전 반복에서 코드를 약간 수정하여 다음과 같은 결과를 얻었습니다.

// DnD to desktop when the Ctrl key is pressed while dragging
if (e.ctrlKey) {
var that = $(e.target);
// make sure it is not IE (attachEvent).
if (that[0].addEventListener) {
that[0].addEventListener("dragstart", function(e) {
    // e.dataTransfer in Firefox uses the DataTransfer constructor
    // instead of Clipboard
    // make sure it's Chrome and not Safari (both webkit-based).
    // setData on DownloadURL returns true on Chrome, and false on Safari
    if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
        e.dataTransfer.setData('DownloadURL', 'http://www.box.net')) {
    var url = (this.dataset && this.dataset.downloadurl) ||
                this.getAttribute("data-downloadurl");
    $.ajax({
        complete: function(data) {
        e.dataTransfer.setData("DownloadURL", data.responseText);
        },
        type:'GET',
        url: url
    });
    }
}, false);
return;
}
}

이건 당연한 일입니다. dragstart 시 즉시 서버에 Ajax를 호출하여 파일의 최신 다운로드 URL을 가져옵니다. 하지만 작동하지 않습니다.

동기 호출 (제가 부르는 명칭은 Sjax)이어야 합니다. 이벤트 리스너가 연결될 때 setData를 실행해야 하는 것 같습니다. jQuery의 API에 따라 강조 표시된 선은 다음과 같이 변경됩니다.

$.ajax({
async: false,
complete: function(data) {
e.dataTransfer.setData("DownloadURL", data.responseText);
},
type: 'GET',
url: url
});

네트워크 연결을 분리할 때까지는 잘 작동했습니다. 동기식 호출을 실행하므로 호출이 성공할 때까지 브라우저가 정지됩니다. Ajax 호출이 실패하면 (404 또는 전혀 응답하지 않음) 브라우저가 비정상 종료된 것처럼 전혀 해동되지 않습니다.

다음과 같이 하는 것이 훨씬 안전합니다.

$.ajax({
async: false,
complete: function(data) {
e.dataTransfer.setData("DownloadURL", data.responseText);
},
error: function(xhr) {
if (xhr.status == 404) {
    xhr.abort();
}
},
type: 'GET',
timeout: 3000,
url: url
});

이 기능을 데모하려면 정적 파일을 Box.net 계정에 업로드해 보세요. Ctrl 키를 누른 상태에서 파일 아이콘을 데스크톱으로 드래그합니다. 계정이 없는 경우 계정을 만드는 데 30초도 채 걸리지 않습니다.

이 기능을 사용하면 창의력을 발휘하여 다양한 작업을 할 수 있습니다. 이미지를 Windows 프린터 대화상자로 드래그하면 즉시 이미지가 인쇄됩니다. Box에서 휴대전화의 드라이브로 노래를 복사하거나 Box에서 IM 클라이언트로 파일을 드래그하여 친구에게 직접 전송할 수 있습니다. 이렇게 하면 생산성을 높이는 무한한 가능성이 열립니다.

프린터에 파일 래깅
파일을 프린터로 드래그합니다.
파일을 IM 클라이언트로 드래그
파일을 IM 클라이언트로 드래그합니다.

의견 및 향후 개선사항

하지만 동기 호출로 인해 브라우저가 잠시 동안 잠길 수 있으므로 여전히 바람직하지는 않습니다. HTML 5 웹 워커도 도움이 되지 않습니다. 웹 워커는 비동기식이어야 하기 때문입니다. 이벤트 리스너가 연결될 때 setData를 실행해야 하는 것 같습니다.

실제로는 성능이 꽤 괜찮습니다. 동기식 Ajax (Sjax) 호출은 URL 문자열을 가져오기만 하므로 속도가 매우 빠릅니다. HTTP 헤더에 큰 오버헤드가 발생하며 이는 WebSocket에서 해결할 수 있습니다. 하지만 이러한 종류의 기술이 더 많이 사용될 때까지는 WebSockets를 사용하여 모든 작은 업데이트를 클라이언트로 전송하는 것은 가치가 없습니다.

향후 API에 다중 파일 다운로드 기능이 추가되기를 바랍니다. 사용자 인터페이스에서 여러 파일을 선택하는 맞춤 체크박스와 함께 사용하면 좋습니다. 또한 제출된 양식의 결과에서 생성된 텍스트 파일과 같이 클라이언트에서 생성한 파일을 이 방법으로 다운로드할 수 있으면 좋습니다.

  • 열 dnd
  • 목록 재정렬
  • 이미지 갤러리 만들기
  • 캔버스 이미지 내보내기

참조