[Do it 장고 + 부트스트랩] 9장. 정적 파일과 미디어 파일 관리하기

정재욱·2023년 6월 10일
0
post-thumbnail

9장에서는 웹 사이트를 꾸며줄 CSS나 이미지 파일 등을 어떻게 처리해야 하는지에 대하여 다루고 있다.

정적 파일 관리하기

포스트 목록 페이지에 부트스트랩 적용하기

blog_list.html 다시 사용하기

4장에서 부트스트랩을 연습할 때 만들었던 blog_list.html 내용을 복사해서 post_list.html에 덮어 씌우자. 그리고 서버를 실행시켜 blog 페이지로 가보자. 그러면 CSS가 적용되지 않은 것을 볼 수 있다.

4장에서 연습할 때는 잘 적용이 되었는데, 지금은 왜 적용이 안된걸까?

F12를 눌러서 개발자 도구를 열고 Console을 봐보면, bootstrap.min.css 파일이 없다고 나와있다.

한 번 부트스트랩을 사용할 때의 기억을 되살려 보자. 웹 브라우저는 먼저 서버에 접속하여 html 파일을 불러온다. html 파일에 css 파일을 사용한다고 명시되어 있다면 그 파일을 불러와서 그 안에 정의된 모양을 적용한 다음, 웹 브라우저 화면에 렌더링한다. 즉, HTML에서 bootstrap.min.css 파일을 불러와서 적용한다.

하지만, 지금은 post_list.html 파일이 있는 곳에 4장에서 했던 것 처럼 bootstrap 폴더를 만들고 bootstrap.min.css 파일을 넣어두고 post_list.html에 bootstrap.min.css의 경로를 넣는다고 해결되지 않는다. 왜냐하면 장고는 모든 경로를 urls.py에서 제어하고 있기 때문이다.

MTV 구조 이해하기

장고는 MTV 구조로 동작한다. 즉, 앱 폴더에 있는 templates 폴더의 html 파일은 views.py에 정의한 내용에 따라 그 빈 칸을 채워 사용자에게 정보를 제공하므로 정적 파일이 아니다. 정적인 파일이 아니라 함은 변화가 있는 파일이라는 뜻이다. 반면 css 파일, jpg 등 이미지 파일 또는 javascript 파일은 따로 변화하지 않는 파일, 즉 변수가 따로 안들어가므로 정적인 파일이라 할 수 있다.
따라서 templates 폴더에 css, js 파일을 함께 넣어 둬도 해당 파일에 접근할 수 없다.

그러면 어떻게 정적 파일을 관리해야 할까?
css, js 파일은 templates 폴더의 html 파일과 달리 고정된 내용만 제공하면 된다. 따라서 최종적으로 웹 서버를 운영할 때는 특정 URL로 접근을 하면 해당 css, js 파일을 제공할 수 있도록 설정해 두면 된다. 그 방법을 알아보자.

static 폴더 만들고 css 파일 넣기

각 앱 폴더 아래에 static 폴더를 만들고 css, js와 같은 정적 파일을 넣는다.

예를 들어 blog/static/blog/bootstrap 폴더를 만든다. 그리고 bootstrap.min.css 파일과 bootstrap.min.css.map 파일을 그 안에 넣어주자.

  • Point. 왜 blog/static/bootstrap/이 아니라 blog/static/blog/bootstrap/ 일까?

    • 나중에 모든 앱들의 static 파일들을 묶어서 관리할건데, static폴더 안에 바로 bootstrap 폴더를 생성해버리면 장고가 헷갈려한다.

    • 즉, app1/static/bootstrap/... , app2/static/bootstrap/... 처럼 static폴더 안에 바로 bootstrap 폴더를 생성한 경우, 모든 앱의 static 파일을 묶으면 어떤 파일이 어떤 앱의 것인지 혼선이 온다.

    • 반면, app1/static/app1/bootstrap/... , app2/static/app2/bootstrap/... 처럼 static 폴더 안에 한번 더 앱 이름의 폴더를 생성해주면, static 파일을 묶었을때 blog/bootstrap, single_pages/bootstrap 처럼 어떤 앱의 파일인지 파악하기 쉽다.

    • 이는 앱의 독립성을 위한 작업이며, templates 폴더 또한 마찬가지이다.

CSS 파일 경로 지정하기

post_list.html 파일에서 <!DOCTYPE html> 바로 아래에 {% load static %}를 추가하여 static 파일을 사용하겠다고 선언한다.

그리고 <head> 태그 안에 원래 bootstrap.min.css 파일 링크가 있던 부분을 다음과 같이 수정하자.

<!DOCTYPE html>
{% load static %}
<html>

<head>
    <title>Blog</title>
    <link href="{% static 'blog/bootstrap/bootstrap.min.css' %}" rel="stylesheet" media="screen">
    <script src="https://kit.fontawesome.com/**********.js" crossorigin="anonymous"></script>
</head>

실제 포스트 내용이 표시되도록 수정하기

지금 post_list.html은 포스트가 2개 있는 것처럼 모양만 만들어 놓은 상태이다.
실제 내용이 보이도록 수정하자.

현재 post_list.html 파일에는 <!-- Blog Post -->로 표시되어 있는 div 요소가 2개 있다. 이 중 하나는 지우고, 앞에서 배운 for 문을 이용하여 다음과 같이 수정하자.

제목이 있어야 하는 자리에 {{ p.title }}, 본문이 있어야 하는 자리에 {{ p.content }}, 작성일이 있어야 하는 자리에 {{ p.created_at }}으로 대체한다.

그리고 <Read More> 버튼 위치의 href에 있던 #을 지우고 {{ p.get_absolute_url }}로 고치자.

<div class="container my-3">
  <div class="row">
    <div class="col-md-8 col-lg-9">
      <h1>Blog</h1>

      {% for p in post_list %} <!-- 추가 -->
      <!-- Blog post-->
      <div class="card mb-4">
        <a href="#!"><img class="card-img-top" src="{{ p.head_image.url }}" alt="..." /></a>
        <div class="card-body">
          <h2 class="card-title h4">{{ p.title }}</h2>  <!-- 수정 -->
          <p class="card-text">{{ p.content }}</p>  <!-- 수정 -->
          <a class="btn btn-primary" href="{{ p.get_absolute_url }}">Read more →</a>  <!-- 수정 -->
        </div>
        <div class="card-footer small text-muted">
          Posted on {{ p.created_at }} by <!-- 수정 -->
          <a href="#">작성자명 쓸 위치(개발예정)</a>
        </div>
      </div>
      {% endfor %}

포스트 상세 페이지에 부트스트랩 적용하기

포스트 상세 페이지에도 부트스트랩을 적용해보자.

Start Bootstrap의 포스트 상세 페이지 디자인 벤치마킹하기

포스트 목록 페이지를 만들 때 활용했던 Start Bootstrap에서 포스트 상세 페이지 디자인을 벤치마킹하자.

필자는 해당 링크에서 index.html 파일을 사용하였다.

Blog Post 디자인 적용하기

해당 내용을 전부 복사한후 blog/templates/blog/post_detail.html의 기존내용을 모두 지우고 붙여 넣는다.

그리고 맨 위에 {% load static %}을 추가하고, bootstrap.min.css를 가져오는 부분을 포스트 목록 페이지를 참고하여 수정하자.

<title>{{ post.title }} - Blog</title>로 title 필드 값이 웹 브라우저 탭의 타이틀이 되도록 수정한다.

마지막으로 js 파일 링크도 원래 있던 jquery.min.js와 bootstrap 관련 코드는 삭제하고, post_list.html에 적용했던 링크로 수정한다.

<!DOCTYPE html>
{% load static %} <!-- 추가 -->
<html lang="ko">

<head>

  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta name="description" content="">
  <meta name="author" content="">

  <title>{{ post.title }} - Blog</title> <!-- 수정 -->

  <!-- Bootstrap core CSS -->
  <!-- 수정 -->
  <link rel="stylesheet" href="{% static 'blog/bootstrap/bootstrap.min.css' %}" media="screen">

  <!-- Custom styles for this template -->
  <link href="css/blog-post.css" rel="stylesheet">

</head>
  
(...생략...)
  <!-- Bootstrap core JavaScript -->
  <!-- 여기부터 -->
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
        integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"
        crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
        integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
        crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
        integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
        crossorigin="anonymous"></script>
  <!-- 여기까지 수정 -->
</body>
</html>

내비게이션 바에 페이지 윗부분이 가려지는 문제 해결하기

웹 페이지에서 확인해보면 포스트 제목이 내비게이션 바에 가려서 보이지 않는다.

이는 <nav> 태그의 class에 추가된 fixed-top 때문이다.
post_list.html과 post_detail.html의 <nav> 태그를 비교해보자.

<!-- blog/templates/blog/post_list.html -->
(..생략..)
<nav class="navbar navbar-expand-lg navbar-light bg-light">
(..생략..)
<!-- blog/templates/blog/post_detail.html -->
(..생략..)
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
(..생략..)

post_detail.html의 <nav> 태그에만 fixed-top이 있다.

이 설정을 지우면 내비게이션 바에 페이지 윗부분이 가려지는 문제를 해결할 수 있다. 하지만 화면을 아래로 내리면 내비게이션 바가 위로 올라가 보이지 않는다.

내비게이션 바를 위에 고정시키면서 페이지 내용도 가려지지 않게 하려면, blog/static/blog 폴더 안에 css 폴더를 만들자. 그리고 해당 링크의 css 폴더에서 blog-post.css 파일을 찾아 넣는다.

blog-post.css 파일은 어떤 역할을 할까?
파일 내용을 살펴보니 <body> 태그의 padding-top을 56픽셀로 설정한 것을 볼 수 있다. 페이지의 윗부분이 내비게이션 바에 가려지므로 그 가려지는 크기만큼 body 요소에 패딩을 주는 간단한 해결책이다.

그리고 해당 css 파일을 사용하기 위해 blog/templates/blog/post_detail.html 파일을 다음과 같이 수정하자.

<!DOCTYPE html>
{% load static %}
<html lang="ko">

<head>

  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta name="description" content="">
  <meta name="author" content="">

  <title>{{ post.title }} - Blog</title>

  <!-- Bootstrap core CSS -->
  <link rel="stylesheet" href="{% static 'blog/bootstrap/bootstrap.min.css' %}" media="screen">

  <!-- Custom styles for this template -->
  <!-- 수정 -->
  <link rel="stylesheet" href="{% static 'blog/css/blog-post.css' %}" media="screen">

</head>

실제 포스트 내용이 표시되도록 수정하기

blog/templates/blog/post_detail.html을 다음과 같이 수정하자.

(..생략..)
  <!-- Page Content -->
  <div class="container">

    <div class="row">

      <!-- Post Content Column -->
      <div class="col-lg-8">

        <!-- Title -->
        <h1 class="mt-4">{{ post.title }}</h1>

        <!-- Author -->
        <p class="lead">
          by
          <a href="#">작성자명 쓸 위치(개발예정)</a>
        </p>

        <hr>

        <!-- Date/Time -->
        <p>Posted on {{ post.created_at }}</p>

        <hr>

        <!-- Preview Image -->
        <img class="img-fluid rounded" src="{{ post.head_image.url }}" alt="">

        <hr>

        <!-- Post Content -->
        <p>{{ post.content }}</p>
        <hr>
(..생략..)

미디어 파일 관리하기

이번에는 이미지를 작성자가 선택해서 업로드할 수 있는 기능을 구현해보자.

포스트에 이미지 올리기

이미지 폴더 지정하기

장고는 이미지 업로드를 위한 ImageField를 제공한다.

ImageField를 사용하려면

  1. 사용자가 업로드한 이미지를 어디에 저장할지 설정.

  2. 업로드된 이미지들이 모여 있는 폴더의 URL을 어떻게 할지 설정해야한다.

우선 settings.py를 열어 맨 아래에 다음과 같이 두 줄을 추가하자. 그리고 os모듈도 사용해야 하니 import 하자.

# do_it_django/setting.py
import os
from pathlib import Path
(..생략..)
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/

STATIC_URL = 'static/'


MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, '_media')

MEDIA_ROOT는 실제로 유저가 올리는 이미지 등의 파일이 올라가는 루트 위치이며, MEDIA_URL은 이 루트 폴더를 어느 url과 맵핑시킬지를 나타낸다.

즉, 이미지 파일은 프로젝트 폴더 아래 _media라는 이름의 폴더 안에 저장하도록 하고, 웹 브라우저에서 도메인 뒤에 /media/라는 경로가 따라오면 미디어 파일을 사용하겠다는 의미다.

이제 blog/models.py를 다음과 같이 수정한다.

from django.db import models


# Create your models here.
class Post(models.Model):  # models 모듈의 Model 클래스를 확장하여 만든 클래스
    """
    포스트의 형태를 정의하는 Post 모델
    제목(title), 내용(content), 작성일(created_at), 작성자 정보(author)
    """
    title = models.CharField(max_length=30)  # CharField : 문자를 담는 필드
    content = models.TextField()  # TextField : 문자열의 길이 제한이 없는 필드

	head_image = models.ImageField(upload_to='blog/images/%Y/%m/%d/', blank=True)
    # upload_to에 이미지를 저장할 폴더의 경로 규칙을 지정, blog/images/연도/월/일/ 저장
    # blank=True -> 해당 필드는 필수 항목은 아니라는 뜻!!!
    # 즉, Post모델의 경우 관리자 페이지에서 title이나 content 필드를 비워 두고 save 버튼을 클릭하면 경고 메시지가 나오지만,
    # blank=True를 설정하면 그 필드를 채우지 않더라고 경고 메시지 없이 저장됨.

    created_at = models.DateTimeField(auto_now_add=True)  # DateTimeField : 월, 일, 시, 분, 초까지 기록하게 해주는 필드
    updated_at = models.DateTimeField(auto_now=True)

    # auto_now_add=True 는 django model 이 최초 저장(insert) 시에만 현재날짜(date.today()) 를 적용
    # auto_now=True 는 django model 이 save 될 때마다 현재날짜(date.today()) 로 갱신

    # author: 추후 작성 예정, 외래키를 구현할 시 다룰 것.

head_image = models.ImageField(upload_to='blog/images/%Y/%m/%d/', blank=True) :

  • upload_to에 이미지를 저장할 폴더의 경로 규칙을 지정. 여기서는 vlog 폴더 아래 images라는 폴더를 만들고, 연도 폴더, 월 폴더, 일 폴더까지 내려간 위치에 저장하도록 설정.

    • 이렇게 여러 폴더로 내려가서 저장하면 서버에서 파일을 찾아오는 시간을 단축할 수 있음!!!
  • blank=True : 해당 필드는 필수 항목은 아니라는 뜻!!!

    • 즉, Post모델의 경우 관리자 페이지에서 title이나 content 필드를 비워 두고 save 버튼을 클릭하면 경고 메시지가 나오지만, blank=True를 설정하면 그 필드를 채우지 않더라고 경고 메시지 없이 저장됨.

Pillow 라이브러리 설치하고 마이그레이션하기

당연하게도 모델을 변경했으니 마이그레이션을 해야한다. ( 익숙해지고 까먹지 말기!! )

단, ImageField를 사용하려면 Pillow 라이브러리가 필요하다.

터미널에 pip install Pillow를 한 이후 마이그레이션을 진행하자.

이미지 업로드 테스트하기

이제 관리자 페이지에 들어가 포스트를 만들어 보면 파일 선택 버튼이 생성된 것을 볼 수 있다.

이미지를 올려보자. 그러면 프로젝트 폴더에 _media 폴더가 생성되고, blog/images/년/월/일 폴더 안에 업로드한 이미지 파일이 저장된 것을 볼 수 있다!!

미디어 파일을 위한 URL 지정하기

그런데 업로드된 이미지 파일의 링크를 클릭해서 접속해보면 'Page not found' 오류가 발생한다.

그 이유는, 아직 urls.py에서 media URL에 대한 설정을 하지 않았기 때문이다.

do_it_django/urls.py 파일을 열어 다음과 같이 수정하자.

from django.contrib import admin
from django.urls import path, include

from django.conf import settings  # 추가
from django.conf.urls.static import static  # 추가

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls')),
    path('', include('single_pages.urls')),
]

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # 추가

static() 함수를 사용하여 미디어 파일에 대한 URL 패턴을 설정한다.
이렇게 하면 Django 개발 서버에서 settings.MEDIA_URL로 시작하는 URL로 접근할 때, settings.MEDIA_ROOT에서 해당 미디어 파일을 찾아서 제공한다.

또한 위 코드에서 urlpatterns에 static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)를 직접 추가하는 대신 urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)를 사용하는 이유는 다음과 같다.

  • urlpatterns은 URL 패턴들의 리스트다. path() 함수를 사용하여 URL 패턴을 추가할 수 있다.
    static() 함수는 URL 패턴이 아니라 미디어 파일을 제공하기 위한 설정이므로, path() 함수를 통해 직접 URL 패턴을 추가하는 것보다는 += 연산자를 사용하여 urlpatterns에 추가하는 것이 더 일관성 있고 명확하기 때문이다.

이제 포스트 목록 페이지에서도 대표 이미지가 보이도록 수정해보자.

post_list.html에서 포스트 이미지에 대한 내용을 담고 있는 코드 한 줄을 다음과 같이 수정하자.

(..생략..)
{% for p in post_list %}
	<!-- Blog post-->
	<div class="card mb-4">
      	<a href="#!"><img class="card-img-top" src="{{ p.head_image.url }}" alt="..." /></a>

단, 주의할 점은 아직 템플릿 파일에서 if문을 사용하는 방법을 배우지 않았기 때문에, 모든 포스트에 대하여 대표 이미지를 업로드 해야 에러가 발생하지 않는다.

_media 폴더를 버전 관리에서 제외하기

_media 폴더는 로컬에서 테스트를 위해 만들어진 폴더이므로 버전 관리를 할 필요가 없다. 또한 불필요한 이미지 파일을 서버로 올려버릴 수도 있다. .gitignore_media/를 추가하자.

포스트 상세 페이지에 이미지 나타내기

이제 포스트 상세 페이지에 이미지가 나타나도록 해보자.
<img> 태그의 이미지 경로(src)를 포스트 목록 페이지에서 했던 것처럼 {{ post.head_image.url }}로 수정하자.

포스트에 파일 올리기

Post 모델에 이미지뿐만 아니라 다른 종류의 파일도 업로드 하고, 필요한 파일을 내려받을 수 있는 기능을 구현해보자.

장고에 있는 FileField를 사용하면 된다.

file_upload 필드 만들기

blog/models.py에 FileField로 file_upload 필드를 추가하자. 사용법은 ImageField와 거의 유사하다. upload_to에는 ImageField에 사용한 경로에서 images 폴더를 files 폴더로만 바꿔준다.

from django.db import models


class Post(models.Model):  # models 모듈의 Model 클래스를 확장하여 만든 클래스
    """
    포스트의 형태를 정의하는 Post 모델
    제목(title), 내용(content), 작성일(created_at), 작성자 정보(author)
    """
    title = models.CharField(max_length=30)  # CharField : 문자를 담는 필드
    content = models.TextField()  # TextField : 문자열의 길이 제한이 없는 필드

    # upload_to에 이미지를 저장할 폴더의 경로 규칙을 지정, blog/images/연도/월/일/ 저장
    # blank=True -> 해당 필드는 필수 항목은 아니라는 뜻!!!
    # 즉, Post모델의 경우 관리자 페이지에서 title이나 content 필드를 비워 두고 save 버튼을 클릭하면 경고 메시지가 나오지만,
    # blank=True를 설정하면 그 필드를 채우지 않더라고 경고 메시지 없이 저장됨.
    head_image = models.ImageField(upload_to='blog/images/%Y/%m/%d/', blank=True)
    
    # 추가!!!!
    file_upload = models.FileField(upload_to='blog/files/%Y/%m/%d/', blank=True)  

    created_at = models.DateTimeField(auto_now_add=True)  # DateTimeField : 월, 일, 시, 분, 초까지 기록하게 해주는 필드
    updated_at = models.DateTimeField(auto_now=True)

    # auto_now_add=True 는 django model 이 최초 저장(insert) 시에만 현재날짜(date.today()) 를 적용
    # auto_now=True 는 django model 이 save 될 때마다 현재날짜(date.today()) 로 갱신

    # author: 추후 작성 예정, 외래키를 구현할 시 다룰 것.
(..생략..)

까먹지 말고!!! 새로운 필드를 만들었으니 마이그레이션을 하자!!

파일 업로드 테스트하기

이제 관리자 페이지에서 포스트를 열어보면 file_upload 필드가 반영된 File upload 입력란이 추가되어있는 것을 확인할 수 있다.

profile
AI 서비스 엔지니어를 목표로 공부하고 있습니다.

0개의 댓글