기본 콘텐츠로 건너뛰기

map과 reduce를 사용하여 distinct(group by) 하는 방법

oracle 같은 RDB 를 다루던 시절엔

user | dept
-----------
john | 0000
tom | 0001
jack | 0000
jane | 0001

과 같은 형태의 테이블일때 부서의 목록을 뽑아 내고자 group by 나 distinct 를 사용하여

dept
-----
0000
0001

이와 같이 뽑아내곤 했는데

noSQL 을 사용하거나 array collection 형태의 데이터를 다룰때에도 이런 작업을 해야할 경우가 있다.

일단 예제로 다뤄볼 대상 collection 의 구조를 가정해 보자.
documents = [
  {
    _id: "........",
    message: "......",
    user : {
      _id: "........",
      profile: {
        displayName: "....."
      }
    }
]
흔한 게시판 형태의 데이터 구조라고 볼 수 있는데
각 documents 마다 고유한 _id 를 가지고 있고 해당 documents 를 작성한 user 객체가 각각 들어있는 경우라고 볼 수 있다.
그렇다면 documents 를 작성한 user 의 목록을 뽑아낼 수는 없을까?

node 콘솔이나 브라우저용 개발 툴에서 복사 붙이기 용으로 코드를 예제 코드를 한번 넣어보자면
documents = [ { _id: "doc0001", message: "document 01", user: { _id: "user0001", profile: { displayName: "john", } } }, { _id: "doc0002", message: "document 02", user: { _id: "user0002", profile: { displayName: "tom", } } }, { _id: "doc0003", message: "document 03", user: { _id: "user0001", profile: { displayName: "john", } } },
{ _id: "doc0004", message: "malformat" },
]
이런 식으로 넣었을때
[
Object
  1. _id: "doc0001"
  2. message: "document 01"
  3. user: Object
    1. _id: "user0001"
    2. profile: Object
      1. displayName: "john"
      2. __proto__: Object
    3. __proto__: Object
  4. __proto__: Object
,
Object
  1. _id: "doc0002"
  2. message: "document 02"
  3. user: Object
    1. _id: "user0002"
    2. profile: Object
      1. displayName: "tom"
      2. __proto__: Object
    3. __proto__: Object
  4. __proto__: Object
,
Object
  1. _id: "doc0003"
  2. message: "document 03"
  3. user: Object
    1. _id: "user0001"
    2. profile: Object
      1. displayName: "john"
      2. __proto__: Object
    3. __proto__: Object
  4. __proto__: Object
,
Object
  1. _id: "doc0004"
  2. message: "malformat"
  3. __proto__: Object
]
요런 구성으로 들어갈 것이다.
마지막 doc0004는 user 가 없는데 당연히 저런 데이터가 있을 수 있다.
왜냐면 관계형 DB 처럼 고정 스키마가 없기 때문이다.

그러면 먼저 map 을 이용하여 user만 추출해보도록 하자.
documents.map(function(v) { return v.user && v.user.profile && v.user.profile.displayName ? v.user:null; });
[
Object
  1. _id: "user0001"
  2. profile: Object
    1. displayName: "john"
    2. __proto__: Object
  3. __proto__: Object
,
Object
  1. _id: "user0002"
  2. profile: Object
    1. displayName: "tom"
    2. __proto__: Object
  3. __proto__: Object
,
Object
  1. _id: "user0001"
  2. profile: Object
    1. displayName: "john"
    2. __proto__: Object
  3. __proto__: Object
, null]
마지막 user가 없는 경우는 조건식을 통해 null을 반환하도록 하였다.
그러면 이제 중복을 제거할 차례다.
중복을 제거하기 위해 reduce 를 사용하자.
reduce는 두개의 인자를 갖는 function을 사용하는데

document.reduce(function(a,b) {
  return ...
});

이런 식으로 사용한다.
reduce 함수의 재미있는 점은 인자를 가져가는 방식이다

index | a | b
0 | array[0] | array[1]
1 | 0번째의 return 값 | array[2]
2 | 1번째의 return 값 | array[3]
......
n-1 | n-2번째의 return 값 | array[n]
------------------------------------
최종값 = n-1번째의 return 값
되시겠다.

이런 특성을 이용하여 중복이 발생하지 않는 user의 array를 만든다면 다음과 같은 전략을 짤 수 있다.

  1. reduce function안에서 a,b 두개를 받는다.
  2. a 즉 지난번 reduce 실행값 중 b가 있는지 검사한다.
  3. 있으면 a 자신을 반환한다.
  4. 없으면 a에 b를 추가하고 a 자신을 반환한다.

단순한 로직인데 a가 일단 array라는 가정이 필요하니 첫번째 시도때 a를 []로 초기화 해줘야하고 b가 a안에 있는지 여부를 검사하기 위해 some 이라는 function 을 사용하여 _id가 같은 것을 발견하면 true를 주게끔 해보자.
아, 그리고 null은 의도한 결과가 아니므로 당연히 예외처리.

documents.map(function(v) { return v.user && v.user.profile && v.user.profile.displayName ? v.user:null; }).reduce(function(a,b) { a=[].concat(a); if(b && !a.some(function(v) { return v._id===b._id; })) { a.push(b); } return a; });
[
Object
  1. _id: "user0001"
  2. profile: Object
    1. displayName: "john"
    2. __proto__: Object
  3. __proto__: Object
,
Object
  1. _id: "user0002"
  2. profile: Object
    1. displayName: "tom"
    2. __proto__: Object
  3. __proto__: Object
]



어떠십니까? 엘레강스하십니까?

쓰고보니 재탕 느낌이 들어서 검색해보니 예전에 이런 글을 쓴적이 있긴 하네
http://spectrumdig.blogspot.kr/2012/03/javascript-union.html
그래도 map이랑 같이 썼으니 인정.


댓글

  1. Array::unique = ->
    output = {}
    output[@[key]] = @[key] for key in [0...@length]
    value for key, value of output

    key,val 을 이용하여 해결하는 방법도 있음. some 을 사용하는 것보다 실행속도가 빠를 듯함.

    답글삭제

댓글 쓰기

이 블로그의 인기 게시물

세상 간단한 https(+secured websocket): Caddy

nginx, apache2 같은 걸로 매번 certbot 연동을 통해 https 하는게 지겨워서 알아보니 Caddy라는게 있더라. 설치법은 강하게 크기 위해 알아서 해본다. # cat /etc/caddy/Caddyfile your.shitty.site:8123 {   proxy / localhost:9123 {     websocket     transparent   } } 이건 외부에서 8123으로 들어오는 걸 내부적으로 9123로 맞춰서 https 맞춰주는 reverse proxy. 만일 port를 생략하면 # cat /etc/caddy/Caddyfile your.shitty.site {   proxy / localhost:9123 {     websocket     transparent   } } 기본포트인 443으로 되어 https://your.shitty.site 로 접속이 된다. 요새 말썽이던 cloudflare 버리고 netlify domain이랑 caddy를 쓰니까 앓던 이가 빠진 느낌. 이 조합은 당분간 계속 써야겠다 싶다.

느려터진 안드로이드 에뮬은 버리고 VM을 쓰자.

iOS개발 환경이 안드로이드보다 우월점은 여러가지가 있겠지만 개인적으로 가장 큰부분이라고 생각하는 점이 iOS Simulator 의 넘사벽 속도다. 사실 iOS 의 경우 Emulator 가 아니라 Simulator 라는 훼이크를 써서 그런건데. 하드웨어+소프트웨어를 같이 하는 회사만이 쓸 수 있는 필살기라 볼 수 있다. 반면 안드로이드의 경우 ARM 에뮬레이터를 사용하는데 이게 참 못만들었다. 플스에뮬이나 GBA에뮬 반정도만 만들어도 써줄텐데 아직 갈길이 멀다. 그래서 구시렁 거리면서 하드웨어를 연결해서 테스트를 하고 있는데 역시 USB연결하는 건 불편하고 apk 를 전송하는 과정도 그다지 빠르지 않아서 개발 생산성이 월등히 나아지지는 않는다. 루팅을 하면 wifi 를 통해 apk 를 인스톨 할 수 있다고 해서 몇 가지를 해보았으나 잘 모르겠지만 인스톨까진 잘 되었는데 디버깅 모드로 실행이 되지 않아 그만두었다. 게다가 전송속도도 USB보다 wifi가 느리고 맘에 들지 않더라. 그러던중 stackoverflow.com(늘 신세지고 있습니다) 에서 "VM으로 안드로이드를 띄워서 adb connect 하면 좋아!" 라는 글에 눈이 번쩍. 시행착오를 몇번 했지만 의외로 간단하더라. 1. VMWare건 VirtualBox건 상관없다. VM호스트를 준비하자. 2. http://www.android-x86.org/download 로 가서  Deprecated x86 2.2 generic  을 받자. Q) 왜 Deprecated 인 2.2 generic 을 받나요. Deprecated는 쓰면 안되는 거 아님? A) http://mariuz.android-dev.ro/vm.iso.7z 도 있다고 한다. http://www.android-x86.org/download 에 있는 요즘 것들은 죄다 안된다. 3. 죄다 일단 Default 설정에 yes yes 하고 설치한다. 한글 문서가 필요한 분은 ...