기본 콘텐츠로 건너뛰기

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. userObject
    1. _id"user0001"
    2. profileObject
      1. displayName"john"
      2. __proto__Object
    3. __proto__Object
  4. __proto__Object
,
Object
  1. _id"doc0002"
  2. message"document 02"
  3. userObject
    1. _id"user0002"
    2. profileObject
      1. displayName"tom"
      2. __proto__Object
    3. __proto__Object
  4. __proto__Object
,
Object
  1. _id"doc0003"
  2. message"document 03"
  3. userObject
    1. _id"user0001"
    2. profileObject
      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. profileObject
    1. displayName"john"
    2. __proto__Object
  3. __proto__Object
,
Object
  1. _id"user0002"
  2. profileObject
    1. displayName"tom"
    2. __proto__Object
  3. __proto__Object
,
Object
  1. _id"user0001"
  2. profileObject
    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. profileObject
    1. displayName"john"
    2. __proto__Object
  3. __proto__Object
,
Object
  1. _id"user0002"
  2. profileObject
    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 을 사용하는 것보다 실행속도가 빠를 듯함.

    답글삭제

댓글 쓰기

이 블로그의 인기 게시물

MQTT Broker Mosquitto 설치 후 설정

우분투 기준 $ sudo apt-add-repository ppa:mosquitto-dev/mosquitto-ppa $ sudo apt-get update 하고 $ sudo apt-get install mosquitto 으로 설치하면 서비스까지 착실하게 올라간다. 설치는 간단한데 사용자를 만들어야한다. /etc/mosquitto/mosquitto.conf 파일에서 권한 설정을 변경하자. allow_anonymous false 를 추가해서 아무나 못들어오게 하자. $ service mosquitto restart 서비스를 재시작. 이제 사용자를 추가하자. mosquitto_passwd <암호파일 경로명> <사용자명> 하면 쉽게 만들 수 있다. # mosquitto_passwd /etc/mosquitto/passwd admin Password:  Reenter password:  암호 넣어준다. 두번 넣어준다. 이제 MQTT 약을 열심히 팔아서 Broker 사글세방 임대업을 하자.

cURL로 cookie를 다루는 법

http://stackoverflow.com/questions/22252226/passport-local-strategy-and-curl 레거시 소스를 보다보면 인증 관련해서 cookie를 사용하는 경우가 있는데 가령 REST 서버인 경우 curl -H "Content-Type: application/json" -X POST -d '{"email": "aaa@bbb.com", "pw": "cccc"}' "http://localhost/login" 이렇게 로그인이 성공이 했더라도 curl -H "Content-Type: application/json" -X GET -d '' "http://localhost/accounts/" 이런 식으로 했을 때 쿠키를 사용한다면 당연히 인증 오류가 날 것이다. curl의 --cookie-jar 와 --cookie 옵션을 사용해서 cookie를 저장하고 꺼내쓰자. 각각 옵션 뒤엔 저장하고 꺼내쓸 파일이름을 임의로 지정하면 된다. 위의 과정을 다시 수정해서 적용하면 curl -H --cookie-jar jarfile "Content-Type: application/json" -X POST -d '{"email": "aaa@bbb.com", "pw": "cccc"}' "http://localhost/login" curl -H --cookie jarfile "Content-Type: application/json" -X GET -d '' "http://localhost/accounts/" 이렇게 사용하면

OS X 터미널에서 tmux 사용시 pane 크기 조절

http://superuser.com/a/660072  글 참조. OS X 에서 tmux 사용시 나눠놓은 pane 크기 조정할 때 원래는 ctrl+b, ctrl+↑←→↓ 로 사이즈를 조정하는데 기본 터미널 키 입력이 조금 문제가 있다. 키 매핑을 다시 하자 Preferences(cmd+,) > Profile >  변경하고자 하는 Theme 선택 > Keyboards 로 들어가서 \033[1;5A \033[1;5B \033[1;5C \033[1;5D 를 순서대로 ↑↓→←순으로 매핑이 되도록 하면 된다. +를 누르고 Key에 해당 화살표키와 Modifier에 ctrl 선택 한 후 <esc>, [, 1, ;, 5 까지 한키 한키 입력 후 A,B,C,D를 써준다. 잘못 입력했을 땐 당황하지 말고 Delete on character 버튼을 눌러 수정하도록 하자. 그리고 다시 tmux에서 ctrl+b, ctrl+↑←→↓로 사이즈를 조절해보자. 잘 된다.