이미지 업로드

장고로 이미지 업로드 하는 방법을 찾아보면 대부분은 모델에 이미지 필드를 추가하고 폼을 생성하여 사용자에게 사진을 받는 방식으로 진행된다. 혹시나 그 방법이 궁금해서 들어온 방문자가 있을까 해당 방법과 동시에 필자가 이번에 찾아본 필드 추가 없이 업로드를 하는 방법을 정리할 것이다.


이미지 필드로 업로드

1
2
3
4
5
6
def avatar_path(instance, filename):
    return 'avatar/u/'+instance.user.username+ '/' + randstr(4) + '.' + filename.split('.')[-1]

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    avatar = models.ImageField(blank=True, upload_to=avatar_path)

만일 사용자의 대표 아바타 이미지를 추가한다고 가정하면 위와같이 이미지 필드를 추가하고 아래와 같이 forms.py에서 업로드할 폼을 만들어 준다.

1
2
3
4
class ProfileForm(forms.ModelForm):
    class Meta:
        model = Profile
        fields = ('avatar',)

이제 위 폼을 views.py를 통해서 사용자에게 폼을 전달해주고 파일을 받아올 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if request.method == 'POST':
    if hasattr(user, 'profile'):
        profile = user.profile
        p_form = ProfileForm(request.POST, request.FILES, instance=profile)
    else:
        p_form = ProfileForm(request.POST, request.FILES)
    if p_form.is_valid():
        profile = p_form.save(commit=False)
        profile.user = user
        profile.save()
        render_args['message'] = '성공적으로 변경되었습니다.'
if hasattr(user, 'profile'):
    profile = user.profile
    p_form = ProfileForm(instance=profile)
else:
    p_form = ProfileForm()
render_args['p_form'] = p_form

뷰에서 위와같이 처리해주면 끝난다! 이미지 필드에 저장된 이미지의 경로가 들어있는 것을 확인할 수 있다.


AJAX로 이미지 업로드

필자는 블로그 서비스를 개발하면서 장고에 익숙하지 않았기에 단순한 이미지 업로드(AJAX)와 같은 경우 PHP를 활용해서 업로드했다. 그러다 오늘 PHP 의존도를 낮추기 위해 해당 기능을 파이썬으로 변경하고자 하였다. 아래의 PHP코드를가Python으로 변환되었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
include "../randstr/randstr.php";

$uploads_url = 'https://MY_DOMAIN.COM/image';
$allowed_ext = array('jpg', 'jpeg', 'png', 'gif');

$uploads_dir = '../../static/image';
$name = $_FILES['image']['name'];
$error = $_FILES['image']['error'];
$ext = strtolower(array_pop(explode('.', $name)));

if(!in_array($ext, $allowed_ext)) {
	echo "허용되지 않는 확장자입니다.";
	exit;
}

if( $error != UPLOAD_ERR_OK ) {
	switch( $error ) {
		case UPLOAD_ERR_INI_SIZE:
		case UPLOAD_ERR_FORM_SIZE:
			echo "파일이 너무 큽니다. ($error)";
			break;
		case UPLOAD_ERR_NO_FILE:
			echo "파일이 첨부되지 않았습니다. ($error)";
			break;
		default:
			echo "파일이 제대로 업로드되지 않았습니다. ($error)";
	}
	exit;
}

$new_name = randstr(20);

if(move_uploaded_file($_FILES['image']['tmp_name'], $uploads_dir.'/'.$new_name.'.'.$ext)){
    echo $uploads_url.'/'.$new_name.'.'.$ext;
} else {
    echo "이미지 업로드를 실패하였습니다.";
}
?> 

PHP의 경우에는 사용자에게 POST받은 순간부터 이미지 업로드가 되는 것 같다. 뭐 당연한 얘기인가… move_uploaded_file로 원하는 위치로 옮기는 것이 핵심 코드며 그 전 코드들은 예외를 처리하기 위해서 작성된 코드다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@csrf_exempt
def image_upload(request):
    if request.method == 'POST':
        if request.FILES['image']:
            allowed_ext = ['jpg', 'jpeg', 'png', 'gif']
            
            image = request.FILES['image']
            ext = str(request.FILES['image']).split('.')[-1].lower()

            if not ext in allowed_ext:
                return HttpResponse('허용된 확장자가 아닙니다.')
                
            upload_path = 'static/image/'

            file_name = randstr(20)
            with open(upload_path + '/' + file_name +'.'+ ext, 'wb+') as destination:
                for chunk in image.chunks():
                    destination.write(chunk)
            
            return HttpResponse('https://MY_DOMAIN.COM/image/' + '/' + file_name +'.'+ ext)
        else:
            return HttpResponse('이미지 파일이 아닙니다.')
    else:
        raise Http404

파이썬의 경우에는 request.FILES를 통해서 파일이 넘어온다. 이걸 원하는 경로에 데이터 기록시켰다. PHP도 내부적으론 이와같은 일이 발생하는 거라고 추측된다. @csrf_exempt은 해당 메서드가 모델로 부터 폼을 전달받는게 아니므로 csrf 보안을 적용시킬 수가 없었기에 해당 메서드에 한해서 보안을 해제시켰다.

WRITTEN BY

배진오

소비적인 일보단 생산적인 일을 추구하며, 좋아하는 일을 잘하고 싶어합니다 :D
im@baejino.com