쾌락코딩

사용자의 TIMEZONE을 고려해 서비스 하기

|

최근 react native로 사이드 프로젝트를 진행하고 있다. 구현중에 timezone을 고려해야 했던 부분이 있어서 고민을 해보았다. 사용자로부터 글을 입력받으면, 그것의 내용과 시간을 데이터베이스에 저장 하고, 다른 screen에서는 해당 사용자가 오늘 적은 모든 글을 뿌려준다. 또한 동시에 다른 screen에서는 하루전, 일주일전, 한달전에 작성한 글을 뿌려주기로 했다.

해외 사용자를 고려해서 서버는 UTC로!

국내 사용자만 고려한다면, 사용중인 데이터베이스의 시간대를 kst로 바꿔서 오늘 날짜 관 관리하면 아~주 편하겠지만 그러지는 않았다. 한국어로 된 서비스라 해외사용자가 사용할 일은 없겠지만 해외에 계신 한국분이 사용할 수 도 있는거고…(물론 한국분들도 사용할지는 모르겠지만 :D)

아마 kst로 사용했다면 오늘 적은 글만 골라서 조회하는 쿼리문은 이렇게 간단했을 것이다. query1

그러나 나는 UTC로 관리하려 했기 때문에, 한국사람이 12월25일 새벽2시에 글을 썻다면, 데이터베이스에는 12월24일 오후 5시로 기록이된다(한국의 UTC offset 시간은 +9시간이기 때문). 이 상태에서 위의 쿼리문을 날린다면 아무런 값도 선택되지 않는다.

moment().utcOffset()

결국에는 사용자의 timezone, utc-offset에 따라서 매번 조회해야 하는 시간대가 다르게 된다는 말이다. 즉, 한국 사용자 12월 25일에 작성한 글만 보고싶다면 쿼리문은 아래와 같아야 한다. query2

2018-12-24 15:00:00는 내가 조회하고 싶은 날짜인 25일에서 한국의 offset인 9를 뺀 시간이다. 만약 한국이 아니라 다른 나라라면, 다른 나라의 offset을 구해서 그 만큼 더해주거나 빼주면 된다.

따라서 결국은 클라이언트에서 서버로 offset값을 전달해 줘야한다. 다행히 moment.js에는 utcOffset()이라는 함수를 제공해주기 때문에 클라이언트의 offset은 아주 쉽게 구할 수 있었다.

클라이언트에서 서버로 요청을 날릴 때, 이 offset을 구해서 서버로 날려주고, 서버에서는 이 offset을 기준으로 연산을해서 쿼리문을 날리면 끝!

##결론 단 하루의 결과를 얻고자 할땐 서버의 코드가 복잡하진 않았다. 그러나 위에서 언급한 것 처럼 하루전, 일주일전, 한달전의 글을 조회할땐 DB query문이 꽤나 복잡해졌다(단순하지만).

사람들에게 물어본 결과로는 내가 한 방법처럼 클라이언트에서 utc-offset값을 넘겨주는건 맞는 방법인 것 같다. 과연 서버의 코드도 더 깔끔하게 할 수 있을지는 더 생각해 봐야 할 부분이다.

express와 Typescript 사용 중 req에 임의의 값 넣기

|

req에 임의의 값이라니?

Typescript와 express를 사용하여 웹 서버를 구축하다보면, req 객체에 임의의 값을 넣어서 사용해야 할 경우가 종종 있다.

예를 들어 사용자 인증에 jwt를 사용하고, 인증이 필요한 addPost라는 라우터가 있다고 가정하면, auth 미들웨어가 사용자 인증을 처리하기 위해 addPost보다 먼저 실행될 것이다.

사용자 요청 -> auth미들웨어 -> appPost 작동

addPost는 auth 미들웨어가 무사히 통과되어야만 도달할 수 있다. auth 미들웨어는 사용자로 부터 받은 jwt토큰을 분석한 사용자 데이터를 req에 넣어 addPost라우터로 넘겨준다. 그럼 addPost는 req로 부터 사용자 데이터를 가져다 쓸 수 있다.

auth 미들웨어의 코드 일부를 보자면 아래와 같다.

import * as express from 'express';
...

const authMidlleWare = 
        async (req: express.Request, res: express.Response, next: express.NextFunction) {
                                    
                ...

const decodedUser: User = await decodeToken(token);
req.decodedUser = decodedUser;
next();

                ...
                                    }

기본설정은 에러가 난다

이 auth 미들웨어에서 에러가 난다. req객체 타입은 express.Request인데 이 타입에는 decodedUser라는 속성이 없기 때문에 타입스크립트는 에러를 낸다.

때문에 우리는 express가 제공하는 타입에 약간 정보를 더해야 한다(customType이라고도 한다). 간단히 decodedUser라는 속성을 넣어주기만 하면 된다.

그러기 위해서 우선 루트에 있는 tsconfig.json을 수정해주어야 한다. 우리가 수정할 부분은 “typeRoots”라는 속성이다.

{
   "compilerOptions": {
    //    "typeRoots" : []
   }
}

기본 설정은 이렇게 주석처리가 되어있고, 기본 값으로 "./node_modules/@types" 디렉터리에서 타입을 읽어온다. 우리가 npm install @types/express로 설치하면 express의 타입은 ./node_modules/@types디렉터리에 존재하게 된다.

이제 우리가 할 일은, express타입을 커스터마이징 할 파일을 한개 만들고, 그 파일을 포함하는 디렉터리 위치를 typeRoots에 입력하면 된다. 예를 들어 ./src/customType폴더에서 custom type 파일을 작성할 계획이라면 tsconfig.json파일은 아래와 같다.

{
   "compilerOptions": {
      "typeRoots": ["./node_modules/@types","./src/customType"], 
   }
}

(typeRoots의 주석이 풀리면 기본값인 ./node_modules/@types도 없어지므로 수동으로 추가해준다.)

참고: 타입스크립트 공식문서(?)를 보면 기본 타입 루트 디렉터리가 ./typings로 되어있는데 이는 옛날에 존재했던 디렉토리라고….지금은 ./node_modules/@types로 바뀐것이라고 하니 알고있자.

커스텀 파일 만들기

이제 ./src/customType 폴더에서 express.d.ts라는 파일을 만들어 req타입에 decodedUser속성을 추가해보자!

// ./src/customType/express.d.ts
import { User } from '../models/User';

declare global {
	namespace Express {
		interface Request {
			decodedUser?: User;
		}
	}
}

이 코드는 Extend Express Request object using Typescript (stackoverflow) 에서 참고 했다.

이로써 express.Request 타입에는 decodedUser가 추가되었다. 이제 addPost에서 req.decodedUser를 호출 할 수 있다.

참고자료

postfix ! (난 undefined가 아니에요)

|

! postfix

! postfix는 해당 프로퍼티가 null 또는 undefined가 아니라는 것을 나타낸다.

타입스크립트가 타입 체킹을 해줌으로써 runtime시점에 발생할 수 있는 많은 에러들을 컴파일 시점에 알려주지만 어쩔수 없이 runtime에 에러가 나는 경우가 있다. 아래 예제를 보자.


interface Person {
    name: string;
    hobby?: string;
}

type eventType = string;

const person: Person = {
    name: 'jun'
};

let eventName = person.hobby; // 'string | undefined' 형식은 string' 형식에 할당할 수 없습니다.

위의 코드를 실행해보면 eventName에 빨간 줄이 그인다(옵션 설정에서 strictNullChecks: true를 했을 경우). person객체에 name 속성은 분명하게 있지만, hobby 속성은 ?가 붙어있어 optional하기 때문에 hobby가 있을 수도, 없을 수도(undefined) 있기 때문이다. 그리고 실제 person 객체에는 hobby가 없기 때문이다.

이런 경우 귀찮지만 아래 코드와 같이 손수 타입 체킹을 해줄 수가 있다.

let eventName;
if (person.hobby) {
	eventName = person.hobby;
}

그러나 개발자가 person객체에 hobby가 있다고 확신한다면? 간단히 ! 접미사를 사용해서 아래와 같이 할 수 있다.

const eventName: eventType = person.hobby!;

이 것은 아래처럼 말하는 것과 같다.

hobby는 null이나 undefined가 아니에요!

물론 정말 null이나 undefined가 아니어야만 한다!

언제 ! 를 쓰나?

개인적으로는 redux + typescript를 사용하면서 Action의 Payload 타입을 지정할 때 써먹었다. 액션의 interface를 보면,

export interface Action<Payload> extends BaseAction {
    payload?: Payload;
    error?: boolean;
}

이렇게 payload가 optional이다. 즉, redux-actions 라이브러리는 payload가 기본적으로 없을 수도 있다고 생각하는 것이다. 물론 정말 payload가 없는 액션도 있지만, payload가 있는 액션을 처리할 때 ! 접미사를 사용한다(이 액션에는 payload가 undefined가 아니에요!).

긴 코드를 제외하고 reducer가 새로운 상태를 반환하는 부분만 보면 아래와 같다.

return {
    ...state,
    category: action.payload!
};

TypeScript Deep Dive 예제

TypeScript Deep Dive (영어)에는 !를 사용하는 다른 예시도 있다.

let a: number[]; // No assertion
let b!: number[]; // Assert

initialize();

a.push(4); // TS ERROR: 값을 할당하기도 전에 사용할 수 없습니다.
b.push(4); // OKAY: because of the assertion

function initialize() {
  a = [0, 1, 2, 3];
  b = [0, 1, 2, 3];
}

! 는 컴파일러에게 당신을 믿으라고 말하는 것이다. 그럼 컴파일러는 코드가 값을 할당하지 않았더라도 불평하지 않을 것이다.

참고자료

binding의 개념과 call, apply, bind의 차이점

|

binding이란?

프로젝트 경험이 거의 없었을 때는 this를 binding한다는 말 조차 이해가 가지 않았었다. javascript기본서에서 call, apply, bind가 나오면 머리가 아팟다. binding이란 도대체 뭘까?

javascript의 함수는 각자 자신만의 this라는 것을 정의한다. 예를 들어 자기소개를 하는 함수를 만들기 위해 say()이라는 함수를 만든다고 하자.

const say = function() {
  console.log(this); // 여기서 this는 뭘까?
  console.log("Hello, my name is " + this.name);
};

say();

실행해보면 this1 window객체가 나타난다. 기본적으로 thiswindow이기 때문이다. 사실 참 어려운게, 꼭 window라고만 말할 수는 없다. this는 객체 내부, 객체 메서드 호출시, 생성자 new 호출시, 명시적 bind시에 따라 바뀌기 때문이다.

어찌되었든 우리는 say함수에서 Window객체를 사용하고 싶지 않다. 즉, this를 그때 그때 알맞은 객체로 바꿔서 this값에 따라 인사말이 할 것이다. 이 것이 this의 binding이다. 명시적으로 위의 this를 Window가 아닌 다른 객체로 바꿔주는 함수가 call, apply, bind이다.

call과 apply

say함수의 this를 변경하고 싶다면, 당연히 this를 대체할 객체가 있어야 한다. 코드를 조금 수정해서 아래와 같이 만들었다. call_apply callapply는 함수를 호출하는 함수이다. 그러나 그냥 실행하는 것이 아니라 첫 번째 인자에 this로 setting하고 싶은 객체를 넘겨주어 this를 바꾸고나서 실행한다.

첫 번째 실행인 say("soeul")의 경우는 say가 실행 될 때 this에 아무런 setting이 되어있지 않으므로 this는 window객체이다.

두 번째 실행인 say.call(obj, "seoul");의 경우와 세 번째 실행인 say.apply(obj, "seoul")은 this를 obj로 변경시켰으므로 원하는 값이 나온다.

call과 apply의 유일한 차이점은, 첫 번째 인자(this를 대체할 값)를 제외하고, 실제 say에 필요한 parameter를 입력하는 방식이다. call과는 다르게 apply함수는 두 번째 인자부터 모두 배열에 넣어야 한다.

bind

boundedFunction bind함수가 call, apply와 다른 점은 함수를 실행하지 않는다는 점이다. 대신 bound함수를 리턴한다. 이 bound함수(boundSay)는 이제부터 this를 obj로 갖고 있기 때문에 나중에 사용해도 된다. bind에 사용하는 나머지 rest 파라미터는 call과 apply와 동일하다.

참고자료

백준 2250번 문제 (트리의 높이와 너비) with Java

|

문제

2250번 문제 링크

이번 포스팅은 문제가 너무 길어서 링크만 걸어놓았다 ㅠㅠ.

2250_3 이 사진만 보며 잘 생각해도 풀 수 있다. 문제 중간중간 계속 이 사진을 보며 푸는게 훨씬 효율적이고 생각 정리가 잘 되었다.

우리가 찾고자 하는 것은 같은 깊이(level)에 속해있는 노드들은 각각 하나의 집합으로 보고, 그 집합의 가장 왼쪽과, 가장 오른쪽 노드의 거리가 가장 먼 그룹의 깊이(level)와 그 너비를 출력하는 것이다. 즉, 위의 예제에서는 level3과 level4의 두 그룹이 너비 18을 가짐으로써 출력할 너비는 18이고, 레벨은 3과 4인데 더 작은 값인 3을 출력하면 된다.

접근법

우선 트리를 만드는 것은 당연하다. 당연한 말이지만 입력으로 들어오는 값을 바탕으로 트리를 만들고나서 어떤 아이디어로 푸느냐가 중요하다.

나같은 경우는 트리의 각 노드를 아래와 같은 클래스로 만들었다.

    static class Node {
        int parent; // 부모 번호
        int num; // 자신의 번호
        int left; // 왼쪽 노드 번호
        int right; // 오른쪽 노드 번호

        Node(int num, int left, int right) {
            this.parent = -1;
            this.num = num;
            this.left = left;
            this.right = right;
        }
    }

Node를 만들 때 우선 모든 parent를 -1로 설정한다. 이 것은 이 문제에 함정이 있기 때문이다. 이 문제에서 루트는 무조건 1부터 시작하지 않는다. 루트노드의 번호가 2,3 또는 100일 수도 있다는 것이다. 그래서 각 노드마다 parent를 -1로 초기화 해놓고, 실제 입력받는 값을 바탕으로 각 Node에 실질적 값을 할당 할 때, parent값을 바꿔줄 것이다. 그러면 나중에 root노드를 찾을 때 편하다. 모든 노드중에 parent가 변하지 않고 여전히 -1인 것을 찾으면, 그게 바로 루트 노드이기 때문이다.

그리고 필요한 것은 각 레벨(깊이)그룹마다 제일 왼쪽에 있는 값의 x좌표 값과, 제일 오른쪽에 있는 값의 x좌표 값이다. 2250_3 위의 그림을 예로 들면 level2의 가장 왼쪽 좌표는 3이고, 가장 오른쪽 좌표는 15니까 너비(width)는 15-3+1이다. 즉, 모든 레벨마다 너비(width)를 구하고 그 중 가장 넓은 값을 출력할 것이기 때문에 각 level마다 좌표의 최소, 최댓값을 저장해야 한다. 그 값들은 아래의 변수에 저장할 것이다.

static int[] levelMin;
static int[] levelMax;

결국 우리는 트리를 천천히 순회하면서 각 레벨의 최소 x좌표, 최대 x좌표 값만 찾으면 된다.

트리를 순회하면서 levelMin, levelMax에 값을 넣어보자.

트리에는 세가지 방법의 순회가 있다.

  • 전위 순회 (루트 -> 왼쪽 -> 오른쪽)
  • 중위 순회 (왼쪽 -> 루트 -> 오른쪽)
  • 후위 순회 (왼쪽 -> 오른쪽 -> 루트)

이 세가지 방법 중에 하나로 트리를 순회해야 하는데 어떤 방법으로 순회하는게 알맞아 보이는가?

중위 순회가 알맞다! 왜냐하면 우리는 트리를 순회하며 모든 노드들을 방문 할 것인데, 중위 순회의 경우는 가장 먼저 방문하는 노드가 가장 왼쪽 노드이기 때문이다. 가장 먼저 방문한 노드의 좌표를 1로 설정한다면 두 번째 방문하게 되는 노드는? 좌표가 2일 것이다. 중위 순회를 하면 방문하는 순서 그대로가 각 노드의 x좌표가 된다!

예시 사진을 보면, 중위 순회로 인해 가장 먼저 방문하는 노드는 8이고, 이를 x좌표 1로 설정한다. 그 다음 방문은 4이며 x좌표는 아까의 좌표에서 1높게 설정하면 그만이다. 이 부분 때문에 제일 윗 부분에서 사진을 보고 문제를 푸는것이 효율적이었다고 말한 것이다.

결국 우리는 중위순회를 하면서 그 노드가 level이 몇이고, 현재 이 노드의 x좌표 값이 같은 레벨에서 가장 왼쪽 값인지, 가장 오른쪽 값인지만 판단하면 되는 것이다. 그리고 가장 왼쪽 값이거나 가장 오른쪽 값이라면? 위에서 선언했던 levelMin[] 배열 또는 levelMax[]을 갱신해주면 된다.

이게 전부다. 이제 전체 코드를 보면서 쉽게 이해할 수 있을 것이다.

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class B2250 {
    final static Scanner scanner = new Scanner(System.in);
    // point : 현재 x좌표 (노드를 방문할 때 마다 +1 증가)
    static int point = 1; 
    static List<Node> tree = new ArrayList<>();
    static int[] levelMin;
    static int[] levelMax;
    static int maxLevel = 0; // 트리의 최대 레벨(깊이)
    public static void main(String[] args) {
        int n = scanner.nextInt();
        levelMin = new int[n+1];
        levelMax = new int[n+1];
        int rootIndex = 0;
        for(int i =0; i<=n; i++) {
            tree.add(new Node(i, -1, -1));
            levelMin[i] = n;
            levelMax[i] = 0;
        }
        for(int i = 0; i < n; i++) {
            int num = scanner.nextInt();
            int left = scanner.nextInt();
            int right = scanner.nextInt();
            tree.get(num).left = left;
            tree.get(num).right = right;
            if(left != -1)  tree.get(left).parent = num;
            if(right != -1) tree.get(right).parent = num;
        }
        for(int i = 1; i<=n; i++) {
            if(tree.get(i).parent == -1) {
                rootIndex = i;
                break;
            }
        }

        inOrder(rootIndex, 1);

        // 완성된 levelMax[]와 levelMin[]을 가지고 값을 뽑아내기
        int answerLevel = 1;
        int answerWidth = levelMax[1] - levelMin[1] + 1;
        for (int i = 2; i<= maxLevel; i++) {
            int width = levelMax[i] - levelMin[i] + 1;
            if(answerWidth < width) {
                answerLevel = i;
                answerWidth = width;
            }
        }
        System.out.println(answerLevel + " " + answerWidth);
    }

    public static void inOrder(int rootIndex, int level) {
        Node root = tree.get(rootIndex);
        if(maxLevel < level) maxLevel = level;
        if(root.left != -1) {
            inOrder(root.left, level + 1);
        }
        // 현재 노드가 가장 왼쪽 노드라면 갱신
        levelMin[level] = Math.min(levelMin[level], point);
        // 현재 노드는 이전노드보다 항상 x좌표가 더 높기 때문에 갱신
        levelMax[level] = point;
        point++;
        if(root.right != -1) {
            inOrder(root.right, level + 1);
        }
    }

    static class Node {
        int parent;
        int num;
        int left;
        int right;

        Node(int num, int left, int right) {
            this.parent = -1;
            this.num = num;
            this.left = left;
            this.right = right;
        }
    }
}

아차!

static List<Node> tree = new ArrayList<>();

이 부분은 그냥 배열로 해도 됐었는데…