최근 서비스 내에 '쇼핑몰 구성'의 기능을 개발 및 런칭 하였습니다. 짧은 일정 속에서 구현해야하는 다양한 조건들을 고민하게 되었고, 그 안에서 어떻게 문제들을 해결했는지 남겨놓고자 합니다.

최초 기획단계에서 고민하였던 방향은, 업데이트 일정을 최소화 하면서, 동시에 상품별로 폼을 커스터마이징 할수 있도록 하는 2가지 조건으로 출발했습니다. 상품을 업로드하는 담당자가 상품의 컨텐츠와 신청폼, 개인정보 3자 제공 동의까지 상품을 완전하게 커스터마이징 가능하도록 구성하는 것이 그중 키 포인트였습니다.

  1. 상품별 컨텐츠 화면을 어떻게 만들까?
  2. 상품별로 다른 신청서 작성을 어떻게 편리하게 구성할까?

상품별 컨텐츠 화면

Django 는 기본적으로 Database에 저장된 text를 브라우저가 렌더링할 수 있는 html 소스로 내뱉는 기능을 제공합니다. 일반적으로는 아래와 같이 프런트 소스를 작성하면 해결됩니다.

<div class="contents">
    {{ instance.contents | safe }}
</div>

위와 같이 입력할 경우 instance 의 contents 가 html 소스인 경우 브라우저에서는 html 소스로 인식하도록 출력됩니다. safe template tag를 사용하지 않는 경우 html소스가 string 그대로 출력됩니다.

이번 업데이트의 경우 컨텐츠 구성의 제약은 크게 2가지였습니다. 상품의 상세 컨텐츠는 이미지와 텍스트가 함께 반영될 수 있도록 구성한다., 내부 팀원이 손쉽게 컨텐츠를 가공할 수 있어야 한다.. 위 2가지 조건이 일반적으로 공존하는 곳은 워드프레스와 같은 블로그이고, 블로그의 컨텐츠 작성툴과 가장 유사하게 구성할 수 있도록 위지윅 에디터를 활용하였습니다.

위지윅 에디터 등을 활용하여 컨텐츠 작성을 진행할 때 작성자에게 있어 불편한 것은, 이미지의 업로드와 관리입니다. 이번 관리화면 개발시에는 기존의 이미지 관리 시스템을 일부 개선하여 구성하였습니다.

  • 에디터가 존재하는 화면 내에 ajax를 기반으로 실시간 이미지 업로드 환경 구성
  • 에디터 상단에 업로드 시간 순서로 이미지 full path 제공
  • CKeditor 를 활용하고, 이미지 플러그인을 추가 적용

위 작업까지는 사전에 충분히 고려했던 부분이고, 기존에 유사하게 구성되고 있는 페이지들이 존재하여 큰 어려움이 없이 진행되었습니다. 특히 html이 담긴 string 을 브라우저에서 그대로 렌더링될수 있도록 함은 Django에서 제공되는 기본 기능으로 편리하게 구성하였습니다.

상품 신청서 커스터마이징 구현

Django 는 기본적으로 Form Class 를 제공하며 Class based Form을 바탕으로 유저 입력값을 관리합니다. Class based Form의 경우 코드베이스이기 때문에 팀원들이 상품별로 다른 폼을 생성할 수가 없는 문제점이 있습니다.

이를 처리하기 위해 JSON을 활용하여 개발자가 아닌 팀원들이 직접 form 을 생성하도록 시스템을 구성해보았습니다.

FLOW
  1. 팀원들이 직접 필요한 입력 필드를 선택하고, 입력필드의 라벨/필수 여부/인풋 설명/필드별 부가 옵션 등을 직접 입력할 수 있도록 구성
  2. 입력 필드의 사전 입력 항목을 정의 (field type, label, name, field option, help text, required, field order)
  3. 선택된 입력 필드들을 JSON string 으로 변환하여 TextField에 저장
  4. 유저 뷰 생성을 위한 함수 (def form\_maker) 작성 (JSON 파싱, html code 생성)
  5. form\_maker 를 활용하여 상품 신청 페이지의 폼 생성
입력 필드 구성 및 정의

입력 필드 생성을 위하여 table 을 활용하고, 각각의 row가 하나의 입력 필드가 되도록 구성하였습니다.

<tr class="wrapper">
    <td>
        <input class="form-control" type="text" name="field_name" 		     autocomplete="off" 
               placeholder="유저에게 보여질 입력값의 명칭을 입력해주세요.">
    </td>
    <td>
        <input class="form-control" type="text" name="field_flag" 
               autocomplete="off" 
               placeholder="영문 구분값 (띄어쓰기 대신 _)">
    </td>
    <td>
        <select class="form-control" name="field_type">
            <option value="text">텍스트 입력</option>
            <option value="date">날짜 입력</option>
            <option value="address">시/도 & 시군구 입력</option>
            <option value="cellphone">휴대폰 번호 입력</option>
            <option value="phone">전화번호 입력</option>
            <option value="select">선택 입력</option>
            <option value="file">파일 첨부 입력</option>
            <option value="textarea">장문 입력</option>
        </select>
    </td>
    <td>
        <input class="form-control" type="text" name="field_option" 
               autocomplete="off" 
               placeholder="각 항목은 | 로 구분해주세요.">
    </td>
    <td>
        <input class="form-control" type="text"  
               name="field_require" autocomplete="off" 
               placeholder="필요하면 True 불필요하면 False.">
    </td>
    <td>
        <input class="form-control" type="text" name="field_guide"           
               autocomplete="off" 
               placeholder="입력폼에 대한 간단한 설명을 입력해주세요.">
    </td>
    <td>
        <button type="button" class="btn btn-warning btn-delete">제거</button>
    </td>
</tr>
POST 된 입력 필드 정의값들을 JSON 화하여 저장
def post(self, request, *args, **kwargs):
    ...
    field_name = request.POST.getlist('field_name')
    field_type = request.POST.getlist('field_type')
    field_option = request.POST.getlist('field_option')
    field_require = request.POST.getlist('field_require')
    field_guide = request.POST.getlist('field_guide')
    field_flag = request.POST.getlist('field_flag')

    formbox = list()
    for i in range(0, len(field_name)):
        obj = dict()
        obj['name'] = field_name[i]
        obj['type'] = field_type[i]
        obj['option'] = field_option[i]
        obj['require'] = field_require[i]
        obj['guide'] = field_guide[i]
        obj['flag'] = field_flag[i]
        formbox.append(obj)

    json_data = json.dumps(formbox)
    instance.form_data = json_data
    ...

form_maker 함수를 활용하여 유저 뷰 생성
@property
def form_maker(self):
    context = dict()
    from django.template.loader import render_to_string
    import json
    fields = json.loads(self.request_form_data)
    for i in fields:
        if i['type'] == 'select':
            i['splitted_option'] = i['option'].split('|')
    context['fields'] = fields

    return render_to_string(
        'store/partials/user_form_maker.html',
        context=context
    )
user_form_maker.html

{% for field in fields %}
    {% if field.type == 'text' %}
        <div class="autonomic-field-box">
            <div class="autonomic-field-label-box">
                <label>
                    {% if field.require == 'True' %}
                    	<span >*</span>
                    {% endif %}
                    {{ field.name }}
                </label>
            </div>
            <div class="autonomic-field-input-box">
                <input {% if field.require == 'True' %}
                            value-require="true"
                       {% endif %} 
                       class="form-control" type="text" 
                       name="{{ field.flag }}">
                {% if field.guide %}
                    <p class="help-block" style="margin-bottom: 0;">
                        {{ field.guide }}
                    </p>
                {% endif %}
            </div>
            <div style="clear:both;"></div>
        </div>
    {% elif field.type == 'date' %}
        ...        
    {% elif field.type == 'address' %}
        ...
form_maker 함수로 유저 뷰 완성하기
<div class="request_form">
    {{ instance.form_maker | safe }}
</div>

이 과정을 통해, 팀원들이 입력한 입력 필드의 정의에 따라 유저들이 입력해야 할 값을 다양하게 구성해줄 수 있습니다. 또, 입력 필드의 종류만 늘려주면 더 다양한 폼 구성이 가능하기 때문에 확장성에도 더욱 용의합니다.

다만, 이렇게 구성할 경우 유저가 입력한 값의 밸리데이션을 어떻게 처리해야 할지 고민해야 합니다. 또, 유저가 입력한 값을 유저에게 다시 보여주는 부분도 구성에 어려움이 될 수 있습니다. 다음 글에서는 밸리데이션 반영과 유저 입력값의 저장 및 출력에 대해서 작성해보겠습니다.