쾌락코딩

AWS s3 AccessDenied error

|

증상

react로 된 정적 파일을 s3로 업로드 하는데 자꾸 AccessDenied error가 떠서 시간을 많이 잡아먹었다.

s3 error

분명히 시키는 대로 Bucket Permissions Policy도 아래와 같이 제대로 설정 했는데도 불구하고 에러가 뜬것이다.

{
    "Version": "2012-10-17",
    "Id": "Policy1533495783554",
    "Statement": [
        {
            "Sid": "Stmt1533495777402",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::elebooks-frontend/*"
        }
    ]
}

원인

내가 멍청한게 원인이라고 할 수 밖에 없다…. 접속 url을 잘 못 알고있던 것이다. aws 공식 문서의 글을 대충 읽고난 후, 내 버킷의 react 앱에 접속 하려면 http://s3-aws-region.amazonaws.com/bucket 혹은 bucketname.s3.ap-northeast-2.amazonaws.com 이런 식의 url로 접속 해야 하는 줄 알고 있었던 것이다.

해결

s3에 올린 정적 웹사이트 접속의 올바른 url은 아래와 같은 곳에 나와있었다. solution

저번에도 같은 실수 때문에 시간을 날렸었기 때문에 한번 적어두었다.

Cloud Watch에서 unable to import module Error

|

증상

클라우드 워치에서

Unable to import module 'dist/app': Error
at Function.Module._resolveFilename (module.js:547:15)
at Function.Module._load (module.js:474:25)
at Module.require (module.js:596:17)
at require (internal/module.js:11:18)
at Object.<anonymous> (/var/task/dist/router/index.js:11:13)
at Module._compile (module.js:652:30)
at Object.Module._extensions..js (module.js:663:10)
at Module.load (module.js:565:32)
at tryModuleLoad (module.js:505:12)
at Function.Module._load (module.js:497:3)

에러가 난다.

원인

클라우드워치에서 unable to import module “dist/app” 과 같이 에러가 뜨는 것은, dist/app을 임포트하지 못하는게 아니라 dist/app에서 실행중인 어떤 것이 import가 안된다는 것. 나는 말그대로 dist/app을 import하지 못하는게 문제라고 생각해서 시간을 많이 뺏겼었는데 결과는 dist/app에서 의존하기 위해 import하는 어떤 것의 경로가 잘못 된 것이었다. 꼭 dist/app에서 바로 import 하는게 아니어도 그렇다. 나의 경우에는, 프로젝트 폴더 구조는 아래 이미지와 같았다.

error1

핵심 역할을 하는 app.js에서의 import 구문은 틀린게 없었는데, router/index.js에서

import user from './user';
import auth from './auth';

이렇게 import를 하는데 폴더 이름이 User, Auth 처럼 되있어서 import 하지 못한 것이 클라우드 워치에서 에러를 unable to import module “dist/app” 이렇게 뿜어준 것이다.(너무 불친절한 에러 표시다ㅠㅠ)

해결

간단히 build/app의 import가 정확이 이뤄질 수 있도록 폴더 이름을 user와 auth로 바꿔서 정상 작동하게 되었다.

나는 src/ 디렉토리를 build 해서 dist파일을 만드는데, babel의 build는 src/ 에서 기존 폴더의 폴더명이 바뀔 경우, dist/ 에서 기존 폴더명을 지우고 새로운 폴더를 만드는게 아니라 기존에 이름이 바뀌기전 폴더를 그대로 놔둔 채로 새로운 폴더를 추가할 뿐이다. 혹시 모르니 자꾸 에러가 난다면 deploy하기 전에 build일을 직접 눈으로 보자!

javascript 프로토타입(prototype) 이해

|

최근들어 거의 대부분을 js로 코딩하고 있음에도 불구하고 제대로 프로토타입을 이해하지 않고 있었다. 사실은 대충 아는 정도였는데, java와 python 코딩을 하면서 class에 너무 익숙해져 있었던 상황에서 얼핏 보았을때 prototype이나 class나 하는 역할은 비슷해 보여서 깊이 공부해보진 않았다. 아무튼 앞으로 js 코딩할 일이 너무 많아질 것 같은 지금 쯔음에 prototype을 공부해보고 정리해 보았다.

자바스크립트는 객체지향 언어인가?

객체지향을 공부할 때면 대부분 java나 c++, c#, python을 이용하여 배운다. 한편, 이런 언어 자체를 배우고싶어서 언어 책을 펴보면 꼭 객체지향 프로그래밍 단원이 포함되어 있으며 모두 클래스를 가지고 있다. 그렇다면 자바스크립트는? 클래스가 없다. 그러나 자바스크립트 역시 객체지향 언어이다. 우리가 집중해야 할 것은 객체지향 프로그래밍이지, 클래스 지향 프로그래밍이 아니다. 객체를 만들 수 있으면 객체지향 언어다. 자바스크립트는 클래스 없이, prototype을 기반으로 객체지향 코딩을 할 수 있게 한다. 그래서 prototype에 대해서 공부해야 한다. java에서 class를 배우는 것 처럼 :D

자바스크립트의 경우 ES6(뭔지 모른다면 최신 자바스크립트라고 생각하면 된다)에서 class문법이 추가되었기 때문에 클래스를 사용하는 다른 언어처럼 클래스 기반으로 코딩할 수 있게 되었다. 그러나 클래스를 흉내낼 뿐, 여전히 내부적으로 prototype을 사용한다. 내 생각이지만 js 개발자들의 많은 수요를 받아들여 클래스 문법이 추가되었다는 것은 prototype보다 class를 활용한 객체지향 코딩이 더 편하고 쉽다는 의미인 것 같기도 하다. 나 역시도 그렇게 느끼고..

prototype을 언제 왜 쓸까?

모든 Javascript 객체는 다른 객체에 대한 참조인 prototype 프로퍼티를 가지고 있다. 내가 이해한 바로는, 가장 큰 이유로 상속을 위해 prototype을 사용한다(사실은 상속보다는 위임에 가깝다). 클래스기반 언어인 자바를 떠올려보자.

  public class Person {
    int head = 1;
    int legs = 2;
  }

이런 클래스가 있다고 치자. 사람이라면 머리가 하나이고 다리가 두 개이니 Person이라는 클래스로 적어놓았다. 그리고 Programmer라는 클래스를 만들어 Person을 상속받아 보겠다.

  public class Programmer extends Person {
    boolean hasLaptop;
  }

이처럼 head나 legs는 사람이라면 모두 해당되는 속성이므로 이를 상속받음으써 코드의 재사용성을 높여주었다. programmer 뿐만 아니라 축구선수, 학생, 요리사 등등의 클래스를 만든다 할지라도 굳이 head나 legs를 선언 해 줄 필요 없이 Person을 상속받기만 하면 된다.

이와 같은 일을 자바스크립트에서는 prototype으로 한다.

자바스크립트로 짠 코드부터 보자

function Person() {}
Person.prototype.head = 1;
Person.prototype.legs = 2;

const lee = new Person();
console.log(lee.legs); // 2;

function Programmer() {}
Programmer.prototype = new Person();
Programmer.prototype.hasLaptop = true;

const song = new Programmer();
console.log(song.head, song.hasLaptop); // 1 true

lee.legs를 출력했더니 마치 Person클래스를 상속받은 것 처럼 2가 출력되었다. prototype이 ‘원형’, ‘기원’ 이라는 뜻임을 보면, 상속이라는 개념과 어울릴 것 같은 느낌이 든다. 위의 코드를 이해하기 위해 조금 더 정리해보겠다.

함수 선언시 내부적으로 일어나는 일

function Person() {}

이렇게 구현부에 내용이 없는 함수를 하나 선언하면 js내부적으로 어떠한 일이 일어난다. 우선, 함수를 선언 했으니 함수가 생성이 될 것이다. Person이라는 함수를 선언 했으니 Person이라는 함수가 메모리에 생성이 되고, 그와 동시에 Person Prototype Object라는 객체가 메모리에 생성이 된다. Person Prototype Object는 말 그대로 객체인데 이게 핵심!

Person.prototype.head = 1;

이렇게 head 값을 주면, 함수를 생성했을 때 자동으로 같이 생성된 Person Prototype Object의 속성으로 저장된다. 그리고 Person 함수로부터 만들어진 모든 객체는 이 Person Prototype Object의 속성으로부터 head값을 받아온다. 즉, Person.prototype 으로 Person Prototype Object를 참조할 수 있다는 이야기. 정말 그런지 브라우저에서 실행한 화면을 보자. prototype1

보는 것 처럼 겨우 Person함수 하나를 선언하기만 했을 뿐인데 로그를 찍어보면 이렇게 많은 내용이 출력된다. js 내부적으로 뭔가가 일어난 것이다.

내용을 보니 prototype이라는 속성이 있다. 이 것이 앞서 말한 Person Prototype Object이다! 메모리 어딘가에 만들어진 이 Person Prototype Object 객체를 참조하고 있는 것이다. 즉 Person Prototype Objectconstructor__proto__라는 속성을 가지고 있는 셈이다. 일반 객체이기 때문에 속성을 마음대로 추가할수 있다. 위에서 본 코드처럼.

Person.prototype.head = 1;

이렇게 코드를 작성한 후에 변화된 내용을 보자. prototype2

Person.prototype, 즉 Person Prototype Object 속성에 head값이 설정된 것이 보인다.

Person Prototype Object의 _proto__는 뭘까?

__proto__는 객체가 생성될 때 조상이었던 함수의 Prototype Object를 가리킨다. 즉, 어떤 객체이든 함수로 부터 생겨나기 때문에 모든 객체는 조상이 되는 함수가 존재할 수 밖에 없는데, 바로 그 함수의 Prototype Object를 가르키는 것이다.

앞에서 첨부한 Person을 로그찍은 사진을 보면 __proto__: Object 라고 되어있는 것을 볼 수 있다. proto**는 객체가 생성될 때 자신의 조상었던 함수의 Prototype Object를 가리킨다고 했었다. 자바스크립트에서는 함수도 일급 객체다. 그럼Person함수(사실은 일급 객체)가 생성 될 때 조상이었던 함수의 Prototype Object가 **proto**라는 것인데, 우리가Person함수를 만들 때 어떤 함수를 상속받지도, new 연산자를 사용하지도 않았는데 도대체Person`함수를 생성할 때 조상이었던 함수가 뭘까?

바로 Object라는 함수이다. 이제막 js를 접하신 분들은 이상하게 들릴 수도 있지만, js의 모든 객체(함수 포함)는 함수로부터 만들어 지는데 자바스크립트에서 기본적으로 이 함수를 Object라는 이름의 함수로써 제공한다. Person함수(객체)의 조상도 Object함수인 것.

이제 또 사진을 보자.

prototype3

아까 분명히

Person.prototype.head = 1;

이 코드를 수행했음에도 불구하고 song 객체를 만들어 로그를 찍어보면 __proto__속성 밖에 없다. 그러나 문제 될 것이 없다! song 객체는 Person 함수로부터 만들어 졌으므로 song__proto__는 Person Prototype Object를 가르키고, 이 Person Prototype Object 안에 모든게 들어있으니까.

console.log(song.head);

와 같이 출력해도 js가 알아서 proto을 뒤져 head값을 찾고 출력해준다. 또 사진을 보자.

prototype4

위 사진처럼 prototype을 건들이지 않고 딱 song객체에만 age를 추가하면 이렇게 결과가 나온다. 이 age는 prototype object에 전혀 영향을 미치지 않는다.

prototype chain

위의 내용을 이해했다면 프로토타입 체인은 아주 쉽게 이해가 가시리라 생각된다. 말 그대로 프로토타입이 체인처럼 이어져 있는 것인데. Person은 Object 함수로 부터 만들어 졌으므로 Person의 prototype은 Object함수이다. 그렇다면 한 단계 더 깊어져서 Programmer라는 함수가 Person을 조상으로 한다면 어떻게 될까? 우선은, 이게 어떻게 가능한 것일까? 코드는 이미 앞에서 보았다.

function Programmer() {};
Programmer.prototype = new Person();

const kim = new Programmer();
console.log(kim.head);

바로 이 코드. 다시 한번 언급하지만 prototype은 메모리 어딘가에 저장된 Prototype Object을 가리킨다고 했었다. 따라서 간단하게 Programmer의 prototype을 Person으로 가리킨게 끝이다. 그럼 Programmer의 조상은 Person이다.

코드 마지막줄의 kim에서 kim.head를 출력했하려고 하는데, kim자신에게는 head가 없다.

kim.head = 1;

이렇게 해준 적이 없으니까. 그래서 Programmer의 prototype Object를 뒤져보는데 또 head가 없다, 그럼 또 그 조상인 Person Prototype Object를 뒤져서 head를 찾아낸다. 이게 바로 prototyhpoe chain이다!

정리

개인적으로 prototype__proto__가 언제 어디서 쓰이는지가 자꾸 헷갈렸다. 정리하자면 prototype을 사용할 수 있는곳은 함수뿐이다. 함수만이 prototype속성을 사용할 수 있다.

Person.prototype; // {head: 1, constructor: 함수, **proto**: 객체}

kim.prototype; // undefined

반면 __proto__는 js의 모든 객체에서 사용된다. 모든 객체는 자신을 만든 조상 객체를 참조할 필요가 있으니까. 함수도 객체이니 함수에서도 사용된다. 함수도 무언가로부터 생성된 객체라는 점을 잊지 말자.

더 좋은 자료

Typescript Quickstart 공부7. 제네릭

|

제네릭 역시 typescript만의 특성이아니라 정적타입을 지원하는 언어, 대표적으로 자바와 동일한 개념이기 때문에 크게 기록할만 한 부분은 없었다. 아주 조금, typescript의 제네릭을 사용할 때 알아두면 좋을 것들, 주의해야 할 사항들만 기록!

제네릭 함수, 클래스 선언 방법

함수나 클래스의 이름 뒤에 선언한다. 정의한 문자열만 할당받을 수 있게 하는 타입.

function arrayConcat<T>(array1: T[], array2: T[]): T[] {
  return array1.concat(array2);
}
let array1: number[] = [1,2,3];
let array2: number[] = [4,5,6];

// resultConcat은 타입 선언이 불필요함.
let resultConcat = arrayConcat<number>(array1,array2);

타입 매개변수 간 연산시 주의 사항

우선 코드를 보자.

function concat<T>(strs: T, strs2: T) {
  // return strs + strs2 에러
  return String(strs) + String(strs2);
}
cancat("abc","123"); // 타입 인수를 생략했기 때문에 타입을 추론해야 함
concat<string>("abc","123");

코드를 보면 strs + strs2는 불가능 하다. 프로그래머는 변수 명도 strs와 같이 문자열임을 알고 있긴 하지만 사실 T로 어떤게 들어올 지는 모르는 일이다. + 연산이 불가능한 객체가 T로 들어올 수도 있는 거니까. 그래서 T T 간 연산이 불가능하다. 그럼 T T 연산을 가능하게 하는 방법을 살펴보자.

오버로드 함수를 이용한 타입 매개변수 간의 연산

오버로드 함수를 이용하면 T+T 와 같은 타입 매개변수 간에 연산이 가능하다.

  • 오버로드 함수: 이름만 같고 매개변수의 타입이나 개수가 다르게 선언된 함수
function concat<T>(strs: T, strs2: T): T;
function concat(strs: any, strs2: any) {
  return strs + strs2;
}
console.log(concat<string>("abc","123)); // abc123

본 함수를 건들지 않고 위에 오버로드 함수를 추가해줌으로써 본 함수가 제네릭 함수가 되도록 했다. 이렇게 하면 strs + strs2 연산도 정상! 추가적으로 다음과 같은 코드는 컴파일 에러가 난다.

console.log(concat("abc", 1111));

함수의 매개변수 타입에는 일치하지만 오버로드 함수에 선언된 타입 매개변수의 형식과 일치하지 않기 때문.

타입 매개변수의 확장 extends

타입 구조 정의 파일을 보면 제네릭의 많은 부분이

<T extends string>

과 같이 extends가 붙는걸 많이 본적이 있다. 이게 뭔가했었는데 설명이 잘 나와있다. 아주 간단한데, T에도 타입을 제약한 것이다. 즉,

<T extends string | number>

이렇게 되어있다면 T는 string 이나 number 밖에 오지 못한다. 그러나 이렇게 한다고 해서 앞선 예제의 strs + strs2가 가능해 지는 것은 아니다.

타입 매개변수에 인터페이스를 상속하기

제네릭 클래스에 전달된 매개변수가 클래스이고 타입 매개변수일 때 코드 어시스트를 받지 못할 때가 있다. 이유는 타입 매개변수 에 타입이 없기 때문이다. 코드 어이스트 지원을 받고 더욱 명시적으로 타입을 선언하려면 처럼 클래스에 대한 인터페이스를 상속해주면 된다.

Typescript Quickstart 공부6. 타입선언과 변경, 타입 호환

|

타입 에일리어스

type alias를 사용하여 기존 타입에 새로운 이름을 지을 수 있다. interface와 비슷해 보인다(사실은 대부분 interface와 class 만드는게 주로 하는 일이라고 하는데, 쓰다 보면 가끔 interface를 type alias로 써도 좋을 때가 있다고 한다).

type myAlias = string | undefined;
type User = {
  id: number;
  alias?: myAlias;
  city: string;
}

타입 에일리어스를 이용한 배열 타입 선언

type MyArrayType = Array<number|boolean>;
let myArray: MyArrayType = [1, true];

타입 추론

타입스크립트에서는 값을 할당할 때 타입을 명시하지 않으면 타입 추론을 통해 타입이 결정된다.

let x = 1; // x의 타입은 number가 된다.

그러나 javascript와는 달리 let 선언이라 할지라도, 처음 값이 할당 될 때 타입이 정해지기 때문에 다른 타입의 값을 할당 할 수는 없다.

// typescript
let first = 1;
let second = "a"; // error

타입 어셜션

타입 어셜션(type assertion)을 이용하면 타입스크립트 컴파일러가 타입 어셜션 정보를 이용해 컴파일을 수행한다. 따라서 타입 어설션은 컴파일 과정까지만 유효하고 컴파일 후에는 사라진다.

또한 형변환과 다르다. 형변환은 실제 데이터 구조를 바꾸는 것이다. 반면 타입 어설션은 ‘타입이 이것이다’라고 컴파일러에게 알려주는 것이다. 대부분의 사용사례는, 더 넓은 범위의 타입이 있을 때, 예를 들어 유니온 타입으로 선언된 변수가 있을 떄 이 변수가 이 환경에서는 딱 한가지 타입으로만 될 수 있는 상황이라고 프로그래머가 확신을 하는 경우에 사용된다.

타입 어설션의 선언 방식은 꺽쇠 괄호 방식과 as 문법을 이용한 방식이 있다.

type myNum = any;

let num1: number = <number>myNum; // 선호되지 않음
let num2: number = myNum as number;

그러나 꺽쇠 기호로 사용하는 타입 어설션은 리액트의 jsx와 혼동되기 때문에 권장되지 않는다. as를 쓰자.