[Flask로 블로그 만들기] 4. 검색과 댓글 기능 구현
검색 기능과 댓글 기능을 한 번에 포스팅하는 이유는 둘 모두 '방문자의 입력을 받는' 동작이기 때문입니다. 어느 서비스든 사용자의 입력을 이용하여 SQL에 쿼리할 때는 신경쓸 것이 많습니다. 지금까지는 이 블로그가 일방적으로 제가 쓴 글을 방문자가 보는 기능만 했는데, 이젠 사용자의 입력을 받는 폼을 사용해야합니다.
검색 기능 구현
검색창은 페이지의 상단 Header에 배치했습니다. 이곳에 사용자가 단어를 입력하면 그 단어가 포함된 포스트를 앞서 만든 글 목록 페이지에 보여주면 됩니다. 이는 MySQL의 LIKE 절을 사용하면 됩니다. SELECT로 뽑아낸 글 제목과 발행일을 보여주기만 하면 되는 간단한 작업입니다.
사용자가 검색어를 입력하는 input 요소를 하나두고, 폼을 제출하는 검색 버튼을 배치합니다. 검색 버튼을 누르면 input 요소에 적은 단어가 아래와 같은 SQL 구문이 되어 질의됩니다.
SELECT * FROM 테이블명 WHERE 글본문 like '%검색어%';
쿼리문을 작성할 때 주의 사항
물론, 실제로 저런식으로 검색어를 직접 쿼리문에 넣으면 안됩니다! 아주 당연하게도 검색어는 서버측에서 검수를 한 후, 유효하지 않은 문자에 대해 이스케이프 처리를 한 다음에 쿼리해야합니다. Python에선 안전하게 쿼리문을 만들 수 있는 모듈을 이용할 수 있기 때문에 이를 이용했습니다. 절대로 쿼리문을 직접 문자열을 연결해서 만들면 안됩니다.
간단하게 아래의 쿼리를 전송한다고 해봅시다.
SELECT * FROM Table WHERE content="ABC"
원래 PEP 249(peps.python.org)와 MySQL Connetor/Python Developer Guide(dev.mysql.com)에 따라 아래와 같이 쿼리할 수 있습니다.
[좋은 예 - 단순한 execute 함수의 사용]
sql = "SELECT * FROM Table WHERE content='ABC'"
cursor.execute(sql)
그러나, 여기서 ABC 부분에 변수를 사용하고자 할 때 아래와 같이 입력을 문자열에 직접 병합시키면 안됩니다.
[나쁜예 - 직접 문자열 병합]
sql = "SELECT * FROM Table WHERE content='" + input_data + "'"
cursor.execute(sql)
Python에서 직접 지원하는 서식 지정자를 이용한 포맷팅도 절대 해서는 안됩니다.
[나쁜예 - 직접 문자열 병합]
sql = "SELECT * FROM Table WHERE content=%s" % input_data
cursor.execute(sql)
대신 아래와 같이 execute()
함수의 두번째 매개변수를 사용하는 것이 좋습니다. 이 경우, 여러개의 데이터를 전달할 경우 리스트로 묶어서 전달할 수 있습니다. 단, 형식 지정자는 항상 %s여야 합니다. 숫자가 온다고 해서 %d를 적으면 안됩니다.
[좋은 예 - 하나의 값 전달]
sql = "SELECT * FROM Table WHERE content=%s"
cursor.execute(sql, input_data)
[좋은 예 - 여러 값 전달 (리스트 이용)]
sql = "SELECT * FROM Table WHERE content=%s or author=%s"
cursor.execute(sql, [input_data1, input_data2])
주의할 점은 아래와 같이 %s 전후로 작은 따옴표를 적는 실수를 할 수 있다는 것입니다. 올바르게 쿼리할 경우 자동으로 문자열은 문자열로 인식하도록 쿼리되므로 변수 중간에 띄어쓰기가 있다고 해서 따옴표를 쿼리문에 직접 적어주지 않아도 됩니다. 이는 오히려 SQL injection에 대해 공격 표면을 증가시킬 수 있습니다.
[나쁜 예 - 형식 지정자를 작은 따옴표로 감싸기]
sql = "SELECT * FROM Table WHERE content='%s'"
cursor.execute(sql, input_data)
특히나 DB에 따라 이스케이프 방법이 다르기 때문에 이렇게 입력값의 전달은 프로그램에게 위임하는 것이 좋습니다. MySQL은 역슬래시(\)를 사용하여 따옴표를 이스케이프 하지만(\"), 다른 DB는 따옴표를 두 번 써서 이스케이프 할 수 있습니다("").
아무튼, Python DB API Spec에 따라 구현된 모듈에선 위와 같이 execute()
함수가 따옴표 처리부터 기타 문자열의 이스케이프 처리까지 알아서 해주기 때문에 이를 이용하는 편이 훨씬 안전합니다. 이를 이용하면 별도의 검증 작업이나 이스케이프 처리가 필요하지 않습니다.
댓글 기능 구현
방문자가 댓글을 쓰면 댓글 테이블에 별도로 댓글 정보를 저장합니다. 포스트를 불러올 때마다 해당 포스트의 ID(Index)를 필터로 하여 해당하는 댓글을 가져오면 됩니다. DB에 삽입하는 과정은 지난 글에서 제가 글쓰기를 구현했던 방법과 동일합니다. 요지는 글을 불러올 때, 아래와 같은 쿼리로 댓글 목록을 가져와서 Flask의 render_template()
함수에 함께 전달해주는 것입니다. 이 과정 역시 지난 글에 있습니다.
SELECT * FROM 댓글테이블 WHERE 글=4
# 4번 글에 달린 모든 댓글을 가져옵니다.
댓글 작성 일시, 작성자 등을 가져온 뒤에 댓글과 대댓글을 구별하여 각각의 변수로 담아 템플릿 파일로 전달해줍니다. 그 후 jinja2가 댓글과 대댓글을 이용하여 댓글 목록을 구현해주는 것입니다. 예를 들어서 아래와 같습니다.
{% for comment in comments %}
// 댓글
{% for childComment in childComments %}
{% if childComment["부모 댓글의 id"] == comment["id"] %}
// 위에서 렌더링한 댓글에 대한 답글
{% endif %}
{% endfor %}
{% endfor %}
이런식으로 이중 반복문을 이용하면 댓글과 그에 달린 답글을 순서대로 보여줄 수 있습니다.
댓글 비밀번호의 저장
댓글은 방문자가 익명으로 관리할 수 있도록 비밀번호를 지정하게 하여 따로 저장해주어야 하는데, 사용자가 입력한 비밀번호는 국가정보원의 검증대상 암호 알고리즘 중 제가 신뢰하는 단방향 해시 함수를 이용하여 솔트를 넣어 해싱합니다. 예를 들어서, 사용자의 비밀번호가 1234이면 이에 Salt를 묻혀 "64!+=54e^$99$1234" 를 해싱하는 것이지요. 흔히들 출력 암호문과 같은 자릿수의 솔트를 사용하는 것이 바람직하다고 하지만, 솔트의 본래 목적과 역할을 봤을 때 암호문의 길이와 상관 없이 32자 정도면 괜찮다 싶습니다.
다만 모든 댓글에 같은 솔트를 쓰면 안됩니다. 만약 대형 사이트의 경우, 모두 같은 솔트를 쓴다면 같은 비밀번호를 입력한 두 사람은 같은 암호문을 갖게 될 것입니다. 공격자는 이를 보고 같은 암호임을 유추할 수 있습니다. 또한, 그 사이트에 맞는 별도의 Rainbow Table을 구성할 수 있을 것입니다. (이런 공격이 효율적인가에 대한 의문은 접어두고, 일단 가능은 하니까요.) 솔트는 매번 달라야 합니다.
다음 글로 계속됩니다. 다음엔 대댓글을 추가하고 댓글을 관리(수정/삭제)하는 기능을 구현해보겠습니다.
시리즈: Flask로 완전 처음부터 블로그 만들기
4. 검색과 댓글 기능 구현 (현재)