백앤드에서 받은 데이터를 어떻게 하면 가공없이 클라이언트에 적용할 수 있을까?

Written by Vallista2019년 2월 25일 · 8 min read

프론트엔드 영역이 날이 갈수록 점점 넓어지고 있습니다. 특히 프론트엔드와 맞닿아 있는 백앤드의 영역은 백앤드 개발자만의 영역이 아닙니다. 이제는 프론트엔드 개발자들도 어느정도 이해를 하고 코드 작성을 하는 영역이 되었습니다.

저희 회사는 개발자가 4명 입니다. 인원이 적은 만큼 백앤드 개발자와 프론트엔드 개발자가 구별이 안되어 있는 상황입니다. 프론트엔드 개발을 할 때 에러가 서버에서 나오는 에러 인 경우에는 프론트엔드 직무를 하는 제가 서버 에러를 고치곤 합니다. 또한 어떤 기능에 대한 이슈가 생겼을때 프론트엔드만 고쳐가면서 힘들게 바꾸기 보다는 서버 로직을 바꾸는게 더 좋은 상황이 나온 경우가 때때로 있어서 프론트엔드와 백앤드는 땔래야 땔 수 없는 영역이라는 생각이 들었습니다.

그래서 더 많은 에러처리 및 안정적 서비스 운영을 위해서는 프론트엔드 개발자들도 필수적으로 백앤드 영역에 대한 이해가 필요하다고 생각합니다.

개발순서 관점에서의 고민

본론으로 들어가서, 백앤드 개발을 하면서 여러분은 개발을 할 때 어떤 순서로 개발을 하시나요? 아래의 두 가지 중에 무엇이 더 낫고 그른지는 상황에 따라 다를겁니다.

상황을 가정하자면 프론트엔드에서 대쉬보드 작업을 해야할 때라고 가정하겠습니다. 대쉬보드에는 전체 사용자의 수, 가입자, 매출, 주문 수 등이 보여야 합니다.

첫 번째 : 서버 로직의 작성 -> 클라이언트 로직의 작성
두 번째 : 클라이언트 로직의 작성 -> 서버 로직의 작성

첫 번째와 두 번째는 순서가 뒤바뀐 차이 뿐입니다.

이 두 가지 프로세스로 진행이 될 때, 이 두 프로세스의 결과물은 전혀 다를 것이라고 생각합니다.

첫 번째 : 개발자 관점에서의 서비스 개발
두 번째 : 유저 관점에서의 서비스 개발

각각의 네이밍을 붙여보았습니다.

개발자 관점에서의 서비스 개발

단순히 쿼리만 먼저 작성한다고 개발자 관점에서의 서비스 개발이 되냐? 라고 제게 다시 물어본다면 저는 "예" 라고 답할 것 입니다.

서비스를 개발하기 전, 기획자와 개발자들은 아이디어를 제시하고 아이디어를 모아 다듬어서 해당 기능 개발에 착수 할 것 입니다. 여기서, 개발을 진행을 할 때 서버를 먼저 작성한다고 생각해봅시다. '아, 전체 사용자의 수, 가입자, 매출, 주문 수 등이 필요하구나.' 고민을 해서 만들었던 쿼리로 API를 만들고 프론트 개발에 들어갈 것 입니다.

프론트 개발에 들어오면 API에서 보내오는 데이터 포맷과 다른 경우가 상당히 많습니다. 이러한 문제는 프론트 레이아웃을 생각하지 않고 데이터만 전달했기 때문입니다. 그렇기 때문에 중간에서 프론트엔드에서 원하는 데이터 폼을 바꿔주거나 API 스펙대로 강제로 레이아웃을 변경해야 합니다.

유저 관점에서의 서비스 개발

그렇다면 유저 관점에서 서비스 개발을 하면 어떤 일이 일어날까요?

서비스를 개발하기 전, 기획자와 개발자들은 아이디어를 제시하고 아이디어를 모아 다듬어서 해당 기능 개발에 착수 할 것 입니다. (여기까진 똑같습니다.) 그 후, 사용자 흐름에 따라 프론트엔드 구성을 시작할 것 입니다. 프론트엔드는 사용자가 사용하는 것이므로 사용자입장에서 일차적으로 고민하게 됩니다. 서버를 먼저 짯을때와는 다르죠. 이미 개발된 서버 데이터를 고민 할 이유가 없으니까요.

이러한 과정을 거친 후, 프론트엔드에서 사용자 중심으로 인터페이스를 제작하고 API에서는 프론트엔드에서 잡힌 규격대로 쿼리를 제작하면 됩니다. 사용자 관점에서 최대한 생각해서 결과를 도출하는 과정이므로 결과물이 많이 다를수도 있습니다. 하지만 정답이 있거나 디자인 및 사용자 관점이 중요하지 않은 경우에는 전자가 더 맞는 개발일 수 있습니다.

프론트엔드에서 잡힌 규격대로 쿼리를 작성하려면

여기부터가 본 글에서 말하려는 중요한 내용이 될 것 같습니다. 프론트엔드에서 잡힌 규격대로 쿼리를 작성하기 위해서는 어떤 쿼리 테크닉이 필요할까요? 개발자 관점에서 이게 필요할 지도 몰라~ 하고 모든 데이터를 전달하면 프론트엔드에서는 서버 데이터를 받고 한번 더 가공해야하는 일이 생깁니다. 그러므로 유저 관점으로 프론트엔드부터 작업을 하면 이 중간 작업을 최대한 안할 수 있는 이야기가 됩니다.

중간 작업을 줄이기 위해서 Vue, React등의 프론트엔드 프레임워크에서 제시한 해법은 Vuex, Redux등의 미들웨어로 감싼 후 거기서 API 통신을 통해 데이터를 가공하여 상태에 넣어줍니다. 하지만 이런 번잡한 작업들을 안하게 된다면 미들웨어를 둘 일이 사라질 수 있는 것이죠.

PostgreSQL에서는 이런 니즈를 구현할 수 있게 훌륭한 Window Function들을 제공합니다. 쿼리를 잘 작성하도록 도와줄 Window Function 몇 가지를 알아보도록 하겠습니다.

Window Function 이름의 유래

Window Function 이름의 유래는, 'SELECT로 검색한 결과에 Window 영역을 넣어서 결과를 한정시킨다' 라는 표현으로 인지하시면 될 것 같습니다. Window 영역을 넣는다는 의미는 영역을 한정시킨다는 뜻 입니다.

예제 쿼리는 PostgreSQL 9.6 버전을 기준으로 합니다.

Window Function

저희가 쿼리 결과를 가공하는데 사용할 Window Function은 row_to_json array_to_json 이렇게 두 가지 입니다.

row_to_json Function

row_to_json은 column 단위로 쪼개진 데이터를 json 포맷으로 묶어줍니다.

console> SELECT row_to_json(order_user.*) FROM order_user
|         row_to_json         |
-------------------------------
| { id: 1, name: "vallista" } |

array_to_json Function

array_to_json은 array 형태의 데이터를 json 포맷으로 바꿔줍니다.

console> SELECT array_to_json(array_agg(row_to_json(order_user.*))) FROM order_user
|         array_to_json         |
---------------------------------
| [{ id: 1, name: "vallista" }] |

array_agg는 PostgreSQL의 Aggregate Function 입니다. array가 아닌 값을 array로 만들어 줍니다.

쿼리문 제작

위에서 언급한 프론트엔드에서 대쉬보드 작업을 해야할 때라고 가정하고 요구사항의 예시를 들어보겠습니다.

요구사항

* 각 결과는 시작 날짜와 종료 날짜의 총합 매출 결과가 있어야 함.
* 각 결과는 시작 날짜와 종료 날짜 사이의 일 단위 매출의 합산 결과가 있어야 함.
  * 매출 합산 결과는 판매처가 총 두 곳이 있어서 두 곳의 결과를 나누어 보여줘야 함.
** 프론트앤드에서 받는 포맷
{
  sales: ?,
  result: [
    {
      date: ?
      a: ?,
      b: ?
    },
    {
      date: ?,
      a: ?,
      b: ?
    },
    {
      date: ?,
      a: ?,
      b: ?
    },
    ...
  ]
}

포맷을 보니 그렇게 복잡하지는 않으나, 처음 접해보는 입장에서는 여간 곤란한게 아닙니다. 단순히 일차원적인 결과값만 보내다가 다른 복잡한 쿼리를 날려보내니 당황스럽기 그지없습니다. 하나하나 천천히 해결해보도록 하겠습니다.

1. sales와 result 각기 다른 타입으로 나누기

처음으로 진행해 볼 작업은 가장 바깥의 결과값을 나누는 작업입니다. 작성을 할 때 기간 설정은 고려하지 않고 먼저 짜보도록 하겠습니다.

sales는 전체 영역에 대해 매출을 더한 값을 가져오도록 하고 result는 일단 빈 배열을 반환해 보도록 하겠습니다.

console> SELECT SUM(price) AS sales, ARRAY[null] AS result FROM item_order
|   sales    |      result       |
----------------------------------
|  13304000  |      {NULL}       |
  • ARRAY[]는 배열을 생성합니다.

간단하네요! 이제 result에 데이터를 넣으면 될 것 같습니다.

2. result에 들어가는 Object 형태의 데이터를 만들기.

result에 바로 배열값을 넣기 전에, 먼저 내부를 이루는 요소들을 제작해보도록 하겠습니다. 배열 요소 하나에 들어가는 데이터는 date, a, b 총 3개 입니다.

console>
SELECT
	date_trunc('days', item_order.created_at) AS date,
	COALESCE(SUM (item_order.price * CASE pickup_place.type WHEN 'a' THEN 1 ELSE 0 END), 0) AS a,
	COALESCE(SUM (item_order.price * CASE pickup_place.type WHEN 'b' THEN 1 ELSE 0 END), 0) AS b
FROM item_order
JOIN pickup_place ON item_order.pickup_place_id = pickup_place.id
GROUP BY date
|          date          | a       | b        |
-----------------------------------------------
| 2018-03-30 00:00:00+09 | 621000  | 0        |
| 2018-04-02 00:00:00+09 | 1898000 | 0        |
| 2018-04-03 00:00:00+09 | 130000  | 0        |
| 2018-04-04 00:00:00+09 | 313000  | 1731000  |
| 2018-04-06 00:00:00+09 | 92000   | 0        |
| 2018-05-04 00:00:00+09 | 78000   | 474000   |
| 2018-05-16 00:00:00+09 | 345000  | 0        |
| 2018-05-21 00:00:00+09 | 5206000 | 0        |
| 2018-05-23 00:00:00+09 | 50000   | 0        |
| 2018-05-24 00:00:00+09 | 150000  | 0        |
| 2018-05-31 00:00:00+09 | 20000   | 58000    |
| 2019-01-06 00:00:00+09 | 0       | 30000    |
| 2019-01-07 00:00:00+09 | 92000   | 2016000  |
  • a, b 등의 판매처 구분 방법은 item_order에 연동된 pickup_place id를 통해 pickup_place에 접근 후 type을 가져와 알 수 있습니다. 그래서 JOIN 쿼리를 통해 pickup_place를 연결하였습니다.

  • date_trunc는 날짜를 첫번째 인자의 타입에 따라서 뒷 부분을 clear 해줍니다. 즉, 'days'면 일 단위 뒤로는 0으로 초기화를 해줍니다.

  • COALESCE는 첫번째 인자부터 NULL 체크를 해서 NULL이면 오른쪽 값을 반환합니다. 즉, (NULL, NULL, 0)이면 순서대로 왼쪽부터 오른쪽까지 진행하여 0르 반환합니다.

  • CASEWHEN에 있는 조건에 따라 TRUE일 시, THEN의 값을 반환하고 다른 조건일 때는 ELSE 값을 반환합니다. 즉 type이 a냐 b냐에 따라서 1과 0을 곱해서 데이터를 반환합니다.

3. 데이터 결과를 json 포맷으로 하나로 묶기

result에서 가져오는 데이터의 형태는 json 포맷을 배열로 감싼 형태입니다. 그러므로 각각의 결과를 json 포맷으로 묶어주어야 합니다.

console>
SELECT row_to_json(sales_array.*)
FROM (SELECT
	date_trunc('days', item_order.created_at) AS date,
	COALESCE(SUM (item_order.price * CASE pickup_place.type WHEN 'a' THEN 1 ELSE 0 END), 0) AS a,
	COALESCE(SUM (item_order.price * CASE pickup_place.type WHEN 'b' THEN 1 ELSE 0 END), 0) AS b
FROM item_order
JOIN pickup_place ON item_order.pickup_place_id = pickup_place.id
GROUP BY date) AS sales_array
| row_to_json                                                |
--------------------------------------------------------------
| {"date":"2018-03-30T00:00:00+09:00","a":621000,"b":0}      |
| {"date":"2018-04-02T00:00:00+09:00","a":1898000,"b":0}     |
| {"date":"2018-04-03T00:00:00+09:00","a":130000,"b":0}      |
| {"date":"2018-04-04T00:00:00+09:00","a":313000,"b":1731000}|
| {"date":"2018-04-06T00:00:00+09:00","a":92000,"b":0}       |
| {"date":"2018-05-04T00:00:00+09:00","a":78000,"b":474000}  |
| {"date":"2018-05-16T00:00:00+09:00","a":345000,"b":0}      |
| {"date":"2018-05-21T00:00:00+09:00","a":5206000,"b":0}     |
| {"date":"2018-05-23T00:00:00+09:00","a":50000,"b":0}       |
| {"date":"2018-05-24T00:00:00+09:00","a":150000,"b":0}      |
| {"date":"2018-05-31T00:00:00+09:00","a":20000,"b":58000}   |
| {"date":"2019-01-06T00:00:00+09:00","a":0,"b":30000}       |
| {"date":"2019-01-07T00:00:00+09:00","a":92000,"b":2016000} |
  • row_to_json을 통해서 json 결과로 묶었습니다.

4. 배열화 하기

result에서는 배열로 가져오기 때문에 배열로 묶어야 합니다. 그러므로 json화 한 값을 배열로 하나로 묶어줘야합니다. 묶어주고 최종 값에 대입해보겠습니다.

console>
SELECT SUM(price) AS sales,
array(
  SELECT row_to_json(sales_array.*)
  FROM (SELECT
  	date_trunc('days', item_order.created_at) AS date,
  	COALESCE(SUM (item_order.price * CASE pickup_place.type WHEN 'a' THEN 1 ELSE 0 END), 0) AS a,
  	COALESCE(SUM (item_order.price * CASE pickup_place.type WHEN 'b' THEN 1 ELSE 0 END), 0) AS b
  FROM item_order
  JOIN pickup_place ON item_order.pickup_place_id = pickup_place.id
  GROUP BY date) AS sales_array
) AS result
|   sales   |	                                 result
-----------------------------------------------------------------------------------------
| 13304000 |	{"{\"date\":\"2018-03-30T00:00:00+09:00\",\"a\":621000,\"b\":0}","...
  • array를 사용해서 배열화를 시켰습니다.

5. array 데이터를 json 포맷으로 변경

결과값이 json형태로 나오지 않다보니 깔끔하게 데이터가 잘려 나오지 않습니다. 이를 array_to_json으로 다시 한번 감싸줘야 할 것 같아 보입니다.

SELECT SUM(price) AS sales,
array_to_json(
	array(
		SELECT row_to_json(sales_array.*)
		FROM (
			SELECT
				date_trunc('days', item_order.created_at) AS date,
				COALESCE(SUM (item_order.price * CASE pickup_place.type WHEN 'a' THEN 1 ELSE 0 END), 0) AS a,
				COALESCE(SUM (item_order.price * CASE pickup_place.type WHEN 'b' THEN 1 ELSE 0 END), 0) AS b
			FROM item_order
			JOIN pickup_place ON item_order.pickup_place_id = pickup_place.id
			GROUP BY date
		) AS sales_array
	)
) AS result
FROM item_order
|   sales   |	                                 result
-----------------------------------------------------------------------------------------
|  13304000 |	[{"date":"2018-03-30T00:00:00+09:00","a":621000,"b":0},...
  • array_to_json으로 result 값을 array 형태에서 json 형태로 바꾸어주었습니다.

이렇게 데이터를 만들어주니 한번 더 편집할 일 없이 프론트앤드에서 바로 받을 수 있는 쿼리를 작성할 수 있게 되었습니다. 다만, 이렇게 작성을 하더라도 프론트앤드는 validation 및 model을 만들어줘야 하니 한번 더 선언을 해야하긴 합니다. 하지만 이런 노력을 기울이면 노가다 코드는 어느정도 줄어들게 되니 개발 시간을 더 효율적으로 사용할 수 있지 않을까요?

마지막으로 정리

  1. 사용자의 시점에서 기획을 하고 프론트앤드 제작을 진행한다, 그 후 백앤드를 개발하면 어느정도 클라이언트 데이터에 맞게 백앤드에서 가공하여 전달할 수 있다. 이는 백앤드를 먼저 개발하고 전달하는 것과는 다르다.

  2. 프론트앤드에서 노가다 코드가 일정량 줄어들게 된다. 이는 작업 효율의 상승을 불러온다.

  3. 하지만 프론트앤드에서는 어쩌나 저쩌나 validation과 modal을 작성해야하므로 작성하면서 데이터를 가공할 수도 있어서 100% 시간 절약을 할 수 있다고 볼 수 없다.