앱 프로젝트 회고

Y·2022년 8월 22일
0

프로젝트

목록 보기
2/5

앱 프로젝트

Day 1

2주간 앱 제작 프로젝트를 진행했다. 정확하게 말하면 앱 출시까지 2주라는 아주 극악(...)의 일정이었기 때문에, 스토어 검토기간까지 생각하면 사실상 개발 일정은 길어야 5일 정도였다. 그래서 개발 일정이 굉장히 빠르고 촉박하게 진행됐다. 우선 나는 백엔드와 데이터 수집을 맡았고, 기술 스택으론 flask를 선택했다. 전혀 써본 적 없는 기술 스택이긴 했는데, 일단 1)스택을 의논할때 안드로이드 개발자분과 나 둘 다 쓸 수 있는 스택이면 편할 거라고 생각했는데, 안드로이드 개발자분이 flask를 사용해본 적 있다고 하셨고, 2)데이터 수집을 파이썬으로 진행하다보니 파이썬 기반 프레임워크면 좋을 것 같다고 생각했으며, 3)ZAPPA를 이용하면 배포가 용이하고, 4)flask자체가 그렇게 어렵지는 않아서 flask로 선택했다. (안드로이드는 kotlin 사용함) 그리고 팀에서 사용할 노션과 깃 레포 파고, flask에 대해서 공부 좀 하고나서 기본 세팅까지 마쳤다. java는 쓸 줄 알지만 kotlin은 써본적 없었어서... flask 보는김에 kotlin도 그냥 간단히 보기는 했다.

Day 2

앱 주제를 정하고, 팀에 디자이너가 없었어서 기획자분이 UI를 담당하시게 됐다. 나는 백엔드를 맡았으므로... API 명세 작성, DB 설계, 데이터 수집을 우선 진행했다. DB는 뭘 사용할지 고민을 하다가 일단 기간 자체가 촉박해서, flask의 경우에는 sqlite가 내장돼있기 때문에 별 생각없이 그냥 sqlite를 그대로 썼다. (지금 와서 생각해보면 다른 DB를 선택하는게 나았을 것 같다...) API 자체는 복잡한 기능을 요구하는 게 없었기 때문에 명세 작성도 금방 끝났고, DB도 사실 복잡할 건 없었기 때문에 설계가 금방 끝났다. DB에서 값 불러와서 클라이언트단으로 보내주는 정도가 전부였기 때문에 SQL 쿼리도 대략적으로 작성을 다 끝내줬다.

크롤링스크래핑(단어를 혼용한 것을 알게됨! 해당 프로젝트의 경우 크롤링보다는 스크래핑을 했다고 표현하는 것이 적절한 것 같다. 이하 모두 스크래핑으로 작성함.)의 경우에는 일단 두 사이트를 하기로 했는데, 데이터 구조도 제각각이고 pandas로만 받아올 수 없는 속성들도 있어서 beautifulsoup로 html을 직접 하나하나 파싱해와서 데이터를 긁어와야했다...그래서 일단 pandas로 받아올 수 있는 값만 test용으로 스크래핑을 했다. 사실 pandas로 받아오고서도 속성 값들이 제대로 들어있지가 않아서 하나하나 문자열 파싱을 따로 해줘야했는데, 그건 일단 둘째치고 DB에 값을 넣고 API를 호출하는 과정이 잘 작동되는지 확인하기 위해서 pandas로 단순하게 페이지의 정보를 받아오고, 그 정보를 DB에 넣는 코드를 짰다.

flask의 경우에는 공식에서 튜토리얼 코드가 있어서 그 코드를 참고하면서 작성을 했는데, 코드를 다 짜고나서 돌려보니까 스크래핑을 한 후 DB에 값을 넣어줘야하는데, DB에 접근이 안 됐다...

 To solve this, set up an application context with app.app_context()

이런 문구만 떴다. 일단 flask를 접한지 얼마 되지도 않은 상태라서 app_context가 뭔지도 모르겠는데 구글링을 해도 모르겠고... 그래서 일단 임시방편으로 Blueprint를 사용해서 해당 url에 접근하면 스크래핑을 돌리는 걸로 코드를 설정했다.

Day 3

데이터 수집 및 문자열 전처리 과정을 진행했다. 데이터 분석을 해본 적은 있지만 2년 전쯤 일이고... 문자열 처리를 하려면 값을 받아와야 되는데 pandas로 하려니 무슨 타입 에러가 너무 많이 떠서 애를 먹었던 것 같다. beautifulsoup로 가져온 데이터같은 경우에는 그냥 str로 바꿔주고 파싱을 하면 되는데, pandas는 그게 잘 안됐다. 특히 split을 한 후에 특정 원소만 선택하는 것(ex.data='데이터1 데이터2'를 data.split(" ")으로 자른 뒤에 [데이터1, 데이터2]로 결과가 나오면 데이터2만 선택하여 테이블 값을 갱신)에서 애를 많이 먹었는데, 다음과 같이 하면 된다.

 for index in indices:
 	if index == 'A' : #'A'라는 이름의 인덱스
    	df[index] = df[index].str.split(" ", 1).str[1] #max_split을 1으로 설정

max_split 설정의 경우는 해당 예시에서는 필요 없을 것 같긴 한데 하다보면 필요할 때가 있다. 아무튼 이런 방식으로 해서 문자열 파싱을 할 수 있었다. (다른 좀 더 쉬운 방법이 있다면 알려주시면 감사하겠습니다!)

Day 4

api에서 값을 불러와 json으로 클라이언트단에 값을 보내줄때, 그냥 jsonify를 사용하니까 오류가 났다... 결국 구글링 끝에 방법을 찾았다.

 db=get_db()
 results=db.execute(~sql query~).fetchall()
 return json.dumps([dict(ix) for ix in results], ensure_ascii=False)

과 같은 방법을 사용하면 된다. 그리고 DB 스키마 작성할때 잘못 작성한 부분이 있어서 (sqlite 문법을 잘못 찾아서) 수정을 좀 했다.

또, 클라이언트단에 데이터를 보내줄때 페이징 처리가 필요하다고 해서 다음과 같은 쿼리문을 사용했다.

 LIMIT 10 OFFSET ~

이걸 하려면 또 page를 받아와야 되는데, 파라미터를 받아오는 것 까진 알겠는데 sql쿼리문에서 변수를 사용하는 방법을 알 수가 없었다... (나는 flask는 처음이었고, django를 한 번 본 적은 있지만 실습코드만 따라서 한 번 해본 정도라 python으로 DB를 다뤄본 건 처음이었다.) ?을 쓰는 방법도 있는데, 그냥 변수를 사용하는 방법이 필요했기 때문에... 아무튼 파라미터를 받아오는 것까지 해서 샘플 코드를 보면 다음과 같다.

 @bp.route('example/<parameter>')
 ...
 page = int(request.args.get('page'))
 offset = (page-1)*10
 results= db.execute("... where ... = :pr limit 10 offset :page",
 {"pr", parameter}, {"page":offset}).fetchall()

같은 방법으로 사용할 수 있다. <parameter>같은 경우는 url을 입력할때 ~/param과 같은 방식으로 바로 입력해주고, arg같은 경우는 ?로 입력해준다. 예컨대 다음과 같다.

  http://localhost.8080/example/par?page=1

으로 넣어주면 parameter변수에는 param이, page 변수에는 1이 들어가는 것이다. 만약 arg가 두개라면 &으로 보내주면 된다. data라는 arg를 하나 더 추가한다고 해보자.

   http://localhost.8080/example/par?page=1&data=2

이런식이다.

물론 변수를 사용할때는 ?을 사용할 수도 있다. 다음과 같다.

   db.execute("~(?)",(parameter))

같은 식이다.

그리고 사용자 위치정보를 받아와서 사용하는 API가 필요해서 kakaoAPI를 사용했는데, 구글링하다보니 PyKakao라는 라이브러리가 있어서 이걸 사용했다.

Day 5

이틀째에 발생 했던 DB 접근 에러를 해결했다. 그냥 create_app()이 있는 메인 함수에서

  from app import 파일명
  with app.app_context():
  	파일명.함수명()

으로 하니까 그냥 정상적으로 작동됐다...

그리고 이때쯤에 시설 정보 중 현재 위치의 구 단위와 일치하는 곳에 존재하는 시설의 정보를 보내주는 API가 필요했는데, 구단위는 수작업으로 입력해야했다..... 그리고 카테고리 분류도 필요했는데, 사실 데이터 양과 시간이 충분하다면 텍스트 마이닝을 쓸 수 있겠지만 우리는 시간도 없고 데이터 양도 충분하지 않기 때문에(현재 유효한 정보만 받아오니 약 240여개정도밖에 되지 않았다.) 수작업말곤 방법이 없었다... 그래서 팀원분과 함께 수작업으로 카테고리를 분류하고 엑셀 파일로 저장한 뒤에 python으로 읽어와서 db에 저장하는 방법을 사용했다.

사실 DB를 자동으로 업데이트해주는 코드를 작성했는데, 데이터 정렬에 문제가 있었다. 1차적으로 페이지 하나를 스크래핑해온 뒤에 DB에 있는 가장 최신의 목록을 찾아서, 그 최신의 목록이 페이지의 최상단에 위치하지 않으면 스크래핑을 새로 해와서 업데이트하는 로직을 사용하면 되는 거였다. 그런데 1)최신의 목록을 어떻게 찾는가 2)업데이트 시 중복 처리의 문제가 있었다. 1의 경우 스크래핑을 최초로 할때 첫 페이지부터 받아오기 때문에 가장 위에 있는 값이 가장 최신이지만, 업데이트를 할때는 업데이트 된 목록들이 가장 아래에 추가될 것이 아닌가.

그렇다면 1)API로 데이터를 보내줄때 최신순 데이터 정렬이 안되고 2)처음 업데이트가 된 이후에는 첫번째에 있는 값이 가장 최신의 값이 아니다. 첫번째로는 데이터를 역순으로 받아오는 방식을 떠올렸는데, 돌리고나서 생각해보니 페이지를 역순으로 받아와도 데이터는 위에서 아래로 받아오니까 여전히 순서는 꼬여있었다. 그러고나서는 데이터 번호(문서목록번호가 페이지 데이터에 존재했다.)를 사용해서 그 번호를 기준으로 그보다 상위 번호의 데이터가 존재하면 스크래핑한 데이터를 저장하는 방식을 사용했다.

그리고 대략적인 API 작업은 끝났기 때문에 서버에 배포를 해야했는데, 안드로이드 개발자분이 ZAPPA라는 걸 쓰면 빨리 배포를 할 수 있다기에 구글링을 해서 진행을 했다. ZAPPA 자체가 배포를 용이하게 하는 도구와 비슷한 거라, 굉장히 빠르고 간단하게 배포를 진행할 수 있는 툴인데... 그런데 안 됐다! ....

flask를 사용하기 위해 디렉터리 구조를 설정하는 방법에는 두 가지 정도가 있는데 1)별도 폴더 만들지 않고 그냥 바로 app.py 사용 2)app 폴더에 __init__.py를 만들고 그 파일에 create_app() 함수 만들어서 사용 이다. 그리고 나는 후자를 사용했다. flask 공식 튜토리얼 문서가 이 방법을 사용하고 있기 때문이다...

그런데 많은 사람들이 1번의 방법을 사용하고, 일단 ZAPPA를 사용한 사람들 자체가 별로 없어 정보 자체가 별로 없기도 했지만... ZAPPA를 사용한 사람들은 거의 1번의 방법을 사용하고 있었다. 이 디렉터리 구조랑 배포랑 무슨 상관인가 하는데 ZAPPA 세팅을 할때는 app_function이라고 실행 함수를 알려줘야한다.

그런데 1번 방법을 사용하는 사람들은 app으로 써주면 끝인데 나같은 경우는 app은 물론이고 app.__init__도 안 되고 app.__init__.create_app도 안 됐다...... 뭘해도 안 됐다. 이제 와서 디렉터리 구조를 뜯어고칠 수도 없고... 서버를 배포를 해야 앱이 돌아가는데 미칠 노릇이었다. 그렇다고 AWS로 쌩으로 배포하기에는 일단 나는 EC2만 사용해봤었기 때문에 lambda를 잘 몰랐다....(서버 비용 문제때문에 서버리스로 하기로 했었음) 그러다가 방법을 찾아냈다! 다음과 같이 진행할 수 있다.

1.가장 상위 디렉터리에서 run.py파일을 만든다.
2.

   from app import create_app
   app=create_app()

을 작성해준다.
3.ZAPPA 환경 세팅에서 app_function은 run.app으로 해준다.

이렇게 하면 정상적으로 배포가 완료된다! 그렇게 해서 배포까지 1차적으로 백엔드 개발을 다 완료를 했다. 아 그리고 배포할때 with app.app_context에 함수들 실행하는 걸 넣어놓으니 오류가 났다. 그냥 db에 값 입력하는 코드들이라서 배포할때 필요한 코드도 아니고... 그래서 빼줬음.

Day 6

데이터중에 시설명이 있고, 사용자 현재 위치 정보(클라이언트단에서 보내줌)를 기반으로 주변에 있는 인근 시설명을 보내주는 API가 있었다. 현재 위치의 반경 몇 km 이내에 어떤 시설이 존재하는지를 검색하는 건 카카오 API를 사용할 수 있는데, 문제점이 있었다.

우선 카카오 API에서 현재 위치를 기반으로 특정 반경 내의 시설을 검색하는 방법은 1)키워드로 장소 검색(시설명 직접 검색) 2)카테고리로 장소 검색 둘 중 하나였다. 그런데 스크래핑으로 긁어오는 데이터들의 양식이 다 다르고, 그 데이터에 있는 시설명과 실제 지도에 있는 시설명이 일치하지가 않는 바람에 애를 꽤 먹었다.

그래서 일단은 특정 카테고리의 시설(ex.카페)만 대상으로 해서 현재 위치 반경 내에 특정 카테고리의 시설 정보를 받아온 후에, 해당 정보 중 시설 DB에 존재하는 정보가 있다면 그 값을 보내주는 식으로 코드를 짜놨었다. 해당 카테고리에 속하는 시설의 위경도로 테스트를 돌렸는데도 반경을 좁히면 제대로 검색이 되지 않는 에러가 발생했다. (반경은 kakao API자체의 문제인지 뭔지 아직 잘 모르겠다...) 그래서 어쩔 수 없이 시설 정보에 있는 모든 시설을 검색하고 해당 반경 내에서 검색이 되면 그 값을 보내주는 식으로 코드를 수정했다. 그런데 당연하게도 이 코드는 진짜 너무 너무 느렸다...ㅎ.... 외부 API를 여러번 실행하는데다 쿼리도 여러번 돌려야돼서 더 그랬다. 그래도 일단 돌아는 가니까 임시방편으로 수정해뒀다.

그리고 데이터에서 카테고리를 DB에 넣어줘야했는데, 카테고리 엑셀 파일에서의 제목 중에 받아온 db의 제목과 일치하는 데이터가 있다면 해당 db의 카테고리 컬럼에 카테고리 엑셀 파일에 존재하는 카테고리명을 넣어주는 식이었다. 그런데 일치여부를 판단하기도 매우..까다로웠다. str.contains같은 방법이 있었는데, 변수를 이용해서 판별하려고 하니 escape문자 때문에 오류가 계속 났다. 그래서 해결한 방법은 다음과 같다.

   tmp = df.loc[df[index명] == bs_list[row]]
   if (tmp.empty == False):
            tmp_result=tmp.iloc[0][category]

제목 데이터 값만을 bs_list에 저장해뒀고, df에는 카테고리 엑셀 파일을 받아온 pandas전체 프레임이 있다. 현재 row의 bs_list 원소의 제목값이 df의 제목 컬럼 데이터들 중에 존재한다면, 이때 df의 해당 제목 컬럼 데이터의 값을 받아오며, 이 값이 empty가 아니라면, 즉 해당 원소 제목값의 컬럼 데이터가 존재한다면 tmp_result에 해당 데이터의 category값을 저장하는 것이다. 말로 하니까 설명이 너무 복잡하지만... 아무튼 pandas에서 특정 변수의 값과 일치하는 컬럼의 데이터값을 찾는 방법이다. 이까지 해결하고 나서 백엔드 개발은 사실상 거의 끝난 상태였다.

Day 7

플레이 스토어에 앱 등록을 했다! 그래서 tag도 찍어줌. 물론 검토기간을 기다려야 했지만... 생각했던 것보다 빨리 앱 개발이 진행됐다. 1일차가 저녁부터 시작했으니 사실상 6일만에 MVP 개발은 다 마친셈이었다. 백엔드 개발은 더 이상 할만한게 별로 없었기 때문에... 검토를 기다리는 동안에 안드로이드 개발을 좀 도왔다. (근데 사실 안드로이드는 둘째치고 kotlin도 써본적이 없었어서 별로 큰 도움은 안 됐다...) 그리고 원래는 클라이언트단에서 현재 위치를 받아와서 kakao api에서 행정구역 주소를 받아오고, 그 행정구역 주소로 api를 요청하면 그 주소 주변의 시설을 검색해주는 api(5일차에 개발했던 것)가 있었는데 클라이언트단에서 kakao api 호출하는데 오류가 발생해서 행정구역 주소 받아오는 것도 백단에서 하기로 했다. 그래서 이 api를 새로 작성했다.

Day 8

백엔드 개발이 사실상 거의 끝나서 테스트 코드를 한 번 작성해보려고 했는데... 튜토리얼을 보면서 테스트 코드를 작성하다가 DB를 날려먹었다.... 물론 새로 스크래핑해오면 되는 거고 로컬 DB라서 서버는 멀쩡하기 때문에 큰 문제는 없었지만 카테고리 같은건 수작업 해줘야돼서 시간이 좀 걸렸다... 튜토리얼 코드에 init_db를 실행하는 코드가 있었는데, 사실 원래 이 튜토리얼 코드는 test용 DB를 따로 만드는 방식이라 그런 거였던 것 같고, 나는 그냥 api 돌아가는지 여부만 테스트하면 되는 거라 실제 db를 테스트 코드에서 사용했다가 이런 대참사가 벌어진 거였다. 테스트 코드 돌릴때는 테스트 DB를 따로 만들거나, 실제 DB 사용시엔 init_db는 꼭 빼도록 하자....(테스트 DB 사용하는게 나을듯...)

그래서 아무튼 그 대참사를 수습하고 나서 테스트 코드를 짜려고 했는데... 사실 API들이 거의 이미 있는 데이터값들 읽어오는 수준이라 딱히 테스트 코드 작성할 만한게 없었다. 그래서 그냥 페이징 갯수 제대로 됐는지 확인하는 코드만 하나 작성했다. 테스트 코드를 어떻게하면 잘 짤 수 있을까... 앞으로도 계속 고민해봐야겠다.

그리고 이때 참사 수습하면서 스크래핑을 새로 돌렸는데 데이터중에 특정 값이 비어있는 경우가 있어서 DB에 삽입할 때 오류가 났다... 그래서 이 부분에 대해서도 코드를 좀 수정해줬다.

Day 9

6일차에 개발했던 현재 위치 정보를 기반으로 주변 시설을 받아오는 API를 수정을 했다. 끔찍하게 느렸기 때문에 수정이 필요했다... 데이터상의 시설 명과 실제 시설명을 연결시켜주는 DB 테이블(실제 시설명은 수작업으로 작성하는 수밖에 없었다.....)을 만든 후에, 실제 시설명을 기준으로 kakao API를 사용해서 위도 경도값을 저장했다. (주소 검색하기 API를 사용하면 실제 시설의 위도 경도를 받아올 수 있다.) 그래서 위도 경도값을 저장한 후에... 현재 사용자 위치의 위도 경도값과 시설들의 위도 경도를 기준으로 거리를 계산하고, 해당 거리를 리스트에 저장했다. 그리고 나서 거리값을 기준으로 리스트를 정렬해주고, 이를 결과값으로 보내주는 방식으로 변경했다. 이전보다는 훨씬 빠른 방식이었다. 그리고 이까지 하고 사실상 백엔드 개발은 진짜로 끝났다.

이후 ~

5일 정도의 검토기간 후에 우리 앱은 별 문제 없이 스토어에 등록됐다. 백엔드단은 딱히 수정할 게 없었다만 클라이언트단을 좀 수정하고 몇 번 업데이트(첫 출시까진 검토가 오래걸리는데, 업데이트는 금방금방 됐다.)도 했다. (그동안 기능이 하나 추가되긴 했지만 클라이언트단에서 구현하기로 해서 딱히 내가 할 일은 없었다.) 사실 앱 제작 기간이 너무 빠듯했어서 여러모로 아쉬움이 남긴 하지만, 그래도 그 기간동안 새로운 기술 스택을 익혀서 앱 출시까지 마쳤다는 것에 의의를 둔다. 그동안은 웹만 했어서 앱 개발은 처음이었는데, 새로 배운 게 꽤 많았다. 고민을 많이 하다가 참여했는데, 정말 좋은 경험이었다고 생각한다. 그리고 이제 완전히 마무리가 됐으니, 앞으로 시간이 나면 리팩토링도 좀 해볼 생각이다.

profile
개발자, 학생

0개의 댓글