lazlojuly의 글 Node.js module.exports vs. exports을 번역했다.


node.js의 module.exports와 exports

(노트: 이 글은 Node.js 6.1.0 릴리즈 이후에 작성되었습니다.)

요약

  • module.exportsrequire() 함수를 사용했을 때 반환 받는 변수라고 생각해봅시다. 비어 있는 객체가 기본값이며 어떤 것으로도 자유롭게 변경할 수 있습니다.
  • exports 자체는 절대 반환되지 않습니다! exports는 단순히 module.exports를 참조하고 있습니다. 이 편리한 변수를 사용하면 모듈을 작성할 때 더 적은 양의 코드로 작성할 수 있습니다. 이 변수의 프로퍼티를 사용하는 방법도 안전하고 추천하는 방법입니다.
exports.method = function () { /* ... */ }
// vs.
module.exports.method = function () { /* ... */ }

간단한 모듈 예제

먼저 예제로 사용할 코드가 필요합니다. 간단한 계산기로 시작합니다.

// calculator.js

module.exports.add = (a, b) => a + b

다음처럼 사용할 수 있습니다.

// app-use-calculator.js

const calculator = require('./calculator.js')
console.log(calculator.add(2, 2) // 출력: 4

모듈 감싸기

Node.js는 require()로 모듈을 불러올 때 함수 래퍼(wrapper) 형태를 사용해 내부적으로 감싸서 호출합니다.

(function (exports, require, module, __filename, __dirname) {
  // calculator.js의 내용은 여기에 추가해 실행합니다.
  module.exports.add = (a, b) => a + b
});

module 객체

변수 **“module”**은 객체로 현재 모듈을 나타냅니다. 이 변수는 각 모듈에 지역적이며 비공개(private) 입니다. (모듈 코드에서만 접근할 수 있습니다.)

// calcualtor-printed.js

module.exports.add = (a, b) => a + b
console.log(module)

// 이 모듈을 require('./calculator-printed.js')로 호출하면 다음과 같은 결과를 볼 수 있습니다.
//
// Module {
//   id: '/Users/laz/repos/module-exports/calculator-printed.js',
//   exports: { add: [Function] },
//   parent: 
//     Module { ... }
//   filename: '/Users/laz/repos/module-exports/calculator-printed.js',
//   loaded: false,
//   children: [],
//   paths: [ ... ]
// }

module.exports

  • 객체 참조로 require() 호출을 하면 받는 값입니다.
  • Node.js에 의해 자동으로 생성됩니다.
  • 일반 JavaScript 객체를 참조합니다.
  • 또한 기본값은 비어 있습니다. (앞서 코드에서는 add() 메소드를 추가했습니다.)

다음 두 방법으로 module.exports를 사용합니다.

  1. 공개(public) 메소드를 붙여 사용합니다. (앞서 작성한 예제가 이 방법입니다.)
  2. 직접 작성한 객체나 함수로 대체하는 방식을 사용합니다.

왜 대체하는 방식으로 사용할까요? 대체하는 방식으로 사용하면 다른 클래스를 임의적으로 수정해 반환하는 것도 가능합니다. 다음 ES2015 예제를 보겠습니다.

// calculator-base.js

module.exports = class Calculator {
  add(a, b) {
    return a + b
  }
  substract(a, b) {
    return a - b
  }
}

이 calculator-base 예제에서는 클래스를 내보냈습니다. 다음 예제에서는 “Calcaulator” 클래스를 확장한 후, 클래스의 개체를 내보냅니다.

// calculator-advanced.js

const Calculator = require('./calculator-base.js')

class AdvancedCalculator extends Calculator {
  multiply(a, b) {
    return a * b
  }
  devide(a, b) {
    return a / b
  }
}

module.exports = new AdvancedCalculator()
// app-use-advanced-calculator.js

const calculator = require('calculator-advanced.js')

console.log(calculator.add(2, 2))      // 출력: 4
console.log(calculator.multiply(3, 3)) // 출력: 9

exports 별칭(alias)

  • exports는 편의 변수로 모듈 작성자가 코드를 덜 작성하도록 돕습니다.
  • 이 변수의 프로퍼티를 사용하는 방식은 안전하고 추천하는 방법입니다. (예를 들면 exports.add = function () { /* ... */ } 식으로)
  • exportsrequire() 함수에서 반환되지 않습니다. (module.exports는 반환하지만요!)

좋은 예제와 나쁜 예제를 확인합니다.

// calculator-exports-exmaples.js

// 좋음
module.exports = {
  add(a, b) { return a + b }
}

// 좋음
module.exports.subtract = (a, b) => a - b

// 가능함
exports = module.exports

// 위에서 작성한 코드보다 간단하고 짧은 코드
exports.multiply = (a, b) => a * b

// 나쁨, exports는 바깥으로 전달되지 않음
exports = {
  divide(a, b) { return a / b }
}

노트: module.exports를 함수나 객체로 대체하는 경우는 일반적인 접근법입니다. 이렇게 대체하면서도 여전히 exports 축약을 사용하고 싶다면 exports를 새로 대체할 객체를 가리키도록 설정해야 합니다. (위에서도 이런 방법을 사용헀습니다.)

exports = module.exports = {}
exports.method = function () { /* ... */ }

결론

변수명은 exports지만 실제로 내보내지 않는다는 사실은 좀 혼란스러울 수 있습니다. 막 node.js를 시작한 사람이라면 특히 그럴겁니다. 공식 문서에서도 이 부분이 조금 이상합니다.

exports와 module.exports의 관계가 마술처럼 느껴진다면 exports는 무시하고 module.exports만 사용하도록 지침을 드립니다.

제 경우엔 이런 코드가 마술이라고 생각하지 않습니다. 개발자라면 사용하는 플랫폼과 언어가 어떻게 동작하는지 깊게 이해하려는 노력을 항상 해야합니다. 이처럼 깊게 이해하는 과정에서 프로그래머는 값진 자신감과 지식을 얻으며 코드 품질, 시스템 구조와 생산성에 긍정적인 영향을 주게 됩니다.

제 글을 읽어주셔서 감사합니다. 의견이나 생각은 언제나 환영하니 덧글로 남겨주세요.

Stephan BoyerWhat are covariance and contravariance?을 번역한 글이다.


공변성과 반공변성은 무엇인가?

서브타이핑은 프로그래밍 언어 이론에서 까다로운 주제다. 공변성과 반공변성은 오해하기 쉬운 주제이기 때문에 까다롭다. 이 글에서는 이 용어를 설명하려고 한다.

이 글에서는 다음과 같은 표기법을 사용한다.

  • A <: BAB의 서브타입이라는 뜻이다.
  • A -> B는 함수 타입으로 함수의 인자 타입은 A며 반환 타입은 B라는 의미다.

동기부여 질문

다음과 같은 세 타입이 있다고 가정하자.

Greyhound <: Dog <: Animal

GreyhoundDog의 서브타입이고 DogAnimal의 서브타입이다. 서브타입은 일반적으로 추이적 관계(transitive)를 갖는다. 그래서 GreyhoundAnimal의 서브타입이라 할 수 있다.

여기서 질문이다. 다음 중 Dog -> Dog의 서브타입이 될 수 있는 경우는 어느 것일까?

  1. Greyhound -> Greyhound
  2. Greyhound -> Animal
  3. Animal -> Animal
  4. Animal -> Greyhound

이 질문에 어떻게 답할 수 있을까? Dog -> Dog 함수를 인자로 받는 f 함수를 살펴보자. 반환 타입에 대해서는 크게 생각하지 않는다. 구체적으로 적어보면 다음과 같다. f : (Dog -> Dog) -> String.

이제 f를 다른 함수인 g와 함께 호출해보자. g에는 위에서 나열했던 각각의 함수를 넣어서 어떤 일이 나타나는지 관찰한다.

g : Greyhound -> Greyhound로 가정하면 f(g)는 타입 안전(type safe)한가?

아니다. 함수 f는 인자 g를 사용하면서 Dog의 다른 서브타입, 예를 들면 GermanShepherd를 사용해서 호출할 수도 있기 때문이다.

g : Greyhound -> Animal로 가정하면 f(g)는 타입 안전한가?

아니다. 1과 동일한 이유다.

g : Animal -> Animal로 가정하면 f(g)는 타입 안전한가?

아니다. f에서 인자 g를 호출하면서 개가 어떻게 짖는지 그 반환 값을 얻으려고 할 수 있다. 하지만 모든 Animal이 짖는 것은 아니다.

g : Animal -> Greyhound로 가정하면 f(g)는 타입 안전한가?

그렇다. 이 경우는 안전하다. f 함수는 인자인 g를 호출할 때 어떤 종류의 Dog든 사용할 수 있다. 모든 DogAnimal이기 때문이다. 또한 반환값은 Dog로 가정할 수 있는데 모든 GreyhoundDog이기 때문이다.

무슨 일이 일어나고 있나요?

결과적으로 다음 경우에 안전하다.

(Animal -> Greyhound) <: (Dog -> Dog)

반환 타입은 간단하다. GreyhoundDog의 서브타입이다. 하지만 인자 타입은 반대다. AnimalDog의 수퍼타입이다!

이 독특한 동작 방식을 적당한 용어를 사용해서 설명한다. 함수 타입에서 반환 타입은 공변적(covariant) 이고, 인자 타입은 반공변적(contravariant) 이다. 반환 타입의 공변성은 A <: B(T -> A) <: (T -> B)로 적용된다는 뜻이다. (A<:의 좌측에, B는 우측에 남아 있다.) 인자 타입의 반공변성은 A <: B(B -> T) <: (A -> T)로 적용된다는 의미다. (AB가 위치를 바꾸게 된다.)

재미있는 사실 타입스크립트에서는 인자 타입이 이변적(bivariant), 즉 공변성과 반공변성을 동시에 지닌다. 뭔가 말이 안되는거 같겠지만 말이다. (TypeScript 2.6부터 --strictFunctionTypes 또는 --strict를 사용하면 이 문제를 해결할 수 있다.) Eiffel은 인자 타입을 반공변적이 아닌 공변적으로 잘못 구현했다.

다른 타입은?

또 다른 질문이다. List<Dog>List<Animal>의 서브타입이 될 수 있을까?

답변하기 좀 미묘하다. 만약 목록이 불변이면 맞다고 답할 수 있다. 하지만 가변적이라면 당연히 안전하지 않다!

이유를 생각해보자. 나는 List<Animal>이 필요한데 List<Dog>를 넘겨줬다고 해보자. 나는 당연히 List<Animal>을 갖고 있다고 생각하고 Cat을 집어넣었다. 이제 List<Dog>Cat이 들어있게 된다! 타입 시스템은 이런 동작을 허용하지 않을 것이다.

불변 목록의 타입이라면 타입 파라미터가 공변적일 수 있지만 가변 목록의 타입은 반드시 공변적이지도, 반공변적이지도 않아야(invariant) 한다.

재미있는 사실 자바에서의 배열은 가변적이면서도 공변적이다. 물론 부적절하다.


추가로, 원문의 덧글 중에 시각적으로 잘 설명하는 자료가 있었다.

요즘 사무실에서 비는 시간이 좀 많이 있어서 책을 가져다두고 읽었다. 가볍게 읽으려고 읽었던 책을 가져가야지 했는데 지금 회사에서는 C#을 전혀 쓰지 않고 있으니 리마인드도 할 겸 읽게 되었다. 베타리딩을 포함해서 3번째 읽는데 그래도 또 배우는 게 많은 건 전에 열심히 안 읽어서 그런 걸까. 이번에는 읽고 기억하고 싶은 키워드라도 적어놔야지 싶어 표시해둔 부분을 여기에 옮겼다.

이 책은 어떤 방식으로 구조를 짜야 좋은 적응력을 가진 코드를 작성할 수 있는지 설명한다. 크게 세 부분으로 볼 수 있는데 가장 먼저 애자일 방법론과 적응형 코드를 작성하기 위한 배경적인 지식을 쌓는다. 그리고 SOLID 패턴에 대해 여러 예시를 들어 설명한 후, 마지막으로 실무에서 적용하는 예시로 마무리한다.

1부에서는 어떤 방식으로 작성하면 코드 의존성이 강해지는지, 어떤 계층적인 접근을 해야 유연한 구조를 구성할 수 있는지 풀어간다. 이런 흐름에서 왜 인터페이스를 도입해야 하고 디자인패턴이 어떻게 코드에 유연함을 더하는지 확인한다. 이런 구조의 코드를 어떻게 테스트하고 리팩토링하는 과정을 통해 개선하는 부분까지 다뤘다.

C#을 기준으로 설명하고 있어서 어셈블리를 어떻게 구성해야 한다거나 nuget을 구성하는 방법이라든가 하는 부분은 좀 거리감 있게 느낄 수도 있는데 요즘은 대다수 언어가 어떤 방식으로든 이런 부분을 지원하고 있으니 그렇게 맥락이 멀게 느껴지지 않았다.

의존성 관리(p. 66)에서 어떤 게 코드 스멜인지 알려주고 그 대안을 잘 설명하고 있다. C#에 매우 한정된 부분이긴 하지만 CLR의 어셈블리 해석 과정도 흥미로웠다.

여기서는 계층화를 점진적으로 진행하는 과정(p. 96)이 특히 좋았다. 인류 발달 과정 설명하듯 하나씩 짚어가며 어떤 이유에서 분리했는지 설명하고 있어서 최종 단계만 보면 막막할 수 있는 계층을 이해하는데 도움이 되었다.

2부에서는 SOLID 패턴을 하나씩 실질적인 예시와 함께 설명했다. 리스코프 치환 원칙을 설명하는 부분(p. 253~)이 재미있었다. 계약 규칙에서는 사전 조건과 사후 조건을 작성하는 방법과 불변성을 어떻게 유지해야 하는지 설명했다. System.Diagnostics.Contracts를 사용해서 각 조건을 기술하는 방식도 참 깔끔하다. 그리고 가변성 규칙에서 공변성과 반공변성을 짚고 넘어갔는데 제네릭이 어떤 식으로 동작하는지 이해하는데 많이 도움 됐다.

인터페이스 분리도 유익했는데 질의/명령을 인터페이스로 분리하는 부분도 좋았다. 의존성 주입은 이미 부지런히 쓰고 있었지만, 생명주기(p. 346)를 설명하는 부분은 다소 모호하게 생각했던 IDispose를 다시 살펴볼 수 있었고 이 인터페이스를 어떤 방식으로 생명주기 관리에 적용하는지 배울 수 있었다. 이 부분은 실무에서 좀 더 많은 사례를 접해보고 싶다.

예전에도 좋아서 추천 많이 했는데 또 읽어도 좋아서 추천하고 싶다. C#도 부지런히 해서 실무에서 다시 쓸 기회가 왔으면 좋겠다.

지금 있는 회사에서도 정말 오래된 php 페이지가 발굴되어 작업해야 하는 경우가 간혹 있다. 예전에는 그냥 MAMP 같은 패키지를 사용해도 큰 문제가 없었다. 이 회사에서는 기본적으로 포함되어 있지 않은 익스텐션을 사용하는 경우가 많아서 (ldap이라든지) 여기 온 이후로는 docker를 많이 사용하고 있다. 물론 배포 환경은 그대로라서 로컬에서만 주로 사용하고 있다. 배포까지 일괄적으로 사용하지 못하는게 좀 아쉽다.

라라벨과 같은 프레임워크를 사용하고 있으면 이미 공개된 docker도 많고 튜토리얼도 찾기 쉽다. 대신 예전 방식으로 작성된 코드를 기준으로 설명하는건 별로 못본 것 같아서 간단하게 정리하려고 한다. 이 글에서는 용어 없이 슥슥 넘어가는 부분이 많다. 또한 실제로 배포 환경까지 사용하지 않고 로컬에서만 사용하더라도 편리한 점이 많다. 그래서 단순히 로컬에서 php 프로젝트를 돌린다는 것 자체에 한정했다. 이 글에서는 docker-compose로 php-apache, mysql 스택을 빠르게 구성하는 방법 을 살펴본다.

도커에 대해 자세히 알고 싶다면 다음 글을 보자.

이 글에서는 다음 이미지를 사용하고 있다.

여기서 사용한 모든 코드는 haruair/docker-simple-php 리포지터리에서 확인할 수 있다.

docker 설치하기

먼저 docker를 받아 설치한다.

환경을 파일로 작성하기

이제 구축할 환경을 파일로 먼저 만든다. 다음 내용에 따라서 docker-compose.ymlDockerfile을 작성한다.

docker-compose.yml 작성하기

docker-compose.yml은 스마트폰을 예로 들면 어떤 앱을 설치하고 앱을 어떻게 설정할지 정리한 파일이다. 1 docker compose라는 도구가 이 파일을 읽어서 앱을 설치하게 된다.

먼저 다음과 같이 작성한다.

version: '3'

services:

services: 아래로는 설치할 이미지를 작성한다.

#...

services:
  db:
    image: mariadb:5.5
    volumes:
      - "./data:/var/lib/mysql:rw"
    environment:
      - "MYSQL_DATABASE=hello"
      - "MYSQL_USER=hello"
      - "MYSQL_PASSWORD=hello"
      - "MYSQL_ROOT_PASSWORD=root"
    ports:
      - "3306:3306"

먼저 db를 추가했다. image를 보면 mariadb의 공식 이미지를 사용했음을 알 수 있다. 이런 이미지는 docker hub를 통해 공유되는데 앱스토어 정도로 생각하면 되겠다. volumes를 사용해서 앱의 경로에 현재 폴더의 ./data를 연결했다. mysql에 생성되는 데이터베이스는 이 폴더에 저장된다.

대부분의 도커 앱은 environment를 통해서 설정을 할 수 있는데 여기서는 데이터베이스명과 사용자, 비밀번호와 루트 비밀번호를 설정한 것을 볼 수 있다. ports는 현재 컴퓨터의 포트와 해당 앱의 포트를 연결하는 설정이다. 즉, 로컬 호스트의 3306 포트로 접속하면 해당 앱의 3306 포트로 접속하는 것과 동일하게 된다.

#...
services:
  #...
  phpmyadmin:
    image: phpmyadmin/phpmyadmin:4.7
    depends_on:
      - db
    ports:
      - "8000:80"
    environment:
      - "PMA_HOST=db"
      - "PMA_USER=root"
      - "PMA_PASSWORD=root"

다음으로 phpmyadmin을 추가했다. phpmyadmin을 사용하지 않는다면 이 부분은 건너뛴다. mysqlworkbench 등으로 접속해도 사용해서 된다. 나는 웹에서 쓸 수 있는 도구를 선호하는 편이라서 phpmyadmin을 개발 환경에 넣어놓는 편이다.

앞서 db와 크게 차이는 없다. depends_on으로 앞에서 추가한 db에 의존하고 있다고 명시했다. 환경변수에 PWA_HOST를 db라고 설정했는데 docker의 앱은 서로 지정된 이름으로 호스트를 참조할 수 있기 때문이다. 사용자명과 비밀번호는 위에서 설정한 대로 설정해서 접근할 수 있도록 했다.

#...
services:
  #...
  web:
    build:
      context: .
      dockerfile: ./Dockerfile
    volumes:
      - ".:/var/www/html"
    depends_on:
      - db
    ports:
      - "80:80"

마지막으로 web을 추가했다. 앞서 추가했던 이미지와 다르게 build가 포함되어 있다. buildDockerfile을 참고해서 이미지를 생성한다. 대부분 도커 앱은 환경변수로도 제어할 수 있지만 앱 안에 확장 기능을 설치하거나 하는 동작도 가능하도록 Dockerfile을 사용할 수 있다. 자세한 내용은 아래서 다룬다. context는 이미지 생성에서 사용할 기본 경로를 지정하는데 사용하며 다음 내용에서 설명한다. Dockerfile의 명칭이 다르거나 경로가 다르면 dockerfile을 통해 지정할 수 있다. volumes에서 현재 프로젝트의 경로를 앱 내 웹서버의 기본 경로에 연결한다. ports에 보면 앱의 80 포트를 로컬의 80포트와 연결했다. http://localhost/를 입력하면 앱에 있는 서버로 연결되게 된다.

작성을 마친 docker-compose.yml 파일은 다음과 같다. php앱을 구동하기 위해서 필요한 환경을 한 위치에 모두 작성했다.

version: '3'

services:
  db:
    image: mariadb:5.5
    volumes:
      - "./data:/var/lib/mysql:rw"
    environment:
      - "MYSQL_DATABASE=hello"
      - "MYSQL_USER=hello"
      - "MYSQL_PASSWORD=hello"
      - "MYSQL_ROOT_PASSWORD=root"
    ports:
      - "3306:3306"
  phpmyadmin:
    image: phpmyadmin/phpmyadmin:4.7
    depends_on:
      - db
    ports:
      - "8000:80"
    environment:
      - "PMA_HOST=db"
      - "PMA_USER=root"
      - "PMA_PASSWORD=root"
  web:
    build:
      context: .
      dockerfile: ./Dockerfile
    volumes:
      - ".:/var/www/html"
    depends_on:
      - db
    ports:
      - "80:80"

Dockerfile 작성하기

앞서 설정에서 작성했던 내용대로 web에 사용하기로 한 Dockerfile을 작성해야 한다. 여기서는 php의 공식 이미지를 기반으로 사용한다. 파일에 다음 내용을 추가하자.

FROM php:5.6-apache

php:5.6-apache를 기반 이미지로 사용했다. Docker hub에 php:5.6-apache로 지정되어 있는 Dockerfile의 내용을 그대로 상속하게 된다. 이제 필요한 패키지와 확장을 설치한다. Dockerfile로 이미지를 생성하면 매 명령마다 중간 이미지를 생성하기 때문에 필수적인 패키지와 확장을 우선순위로 두면 나중에 비슷한 이미지를 생성할 때 더 빠르게 동작한다.

# ...
RUN apt-get update
RUN apt-get install -y git zip

RUN apt-get install -y libpng12-dev libjpeg-dev
RUN apt-get install -y mysql-client
RUN docker-php-ext-configure gd --with-png-dir=/usr --with-jpeg-dir=/usr \
&& docker-php-ext-install gd

RUN docker-php-ext-install mbstring
RUN docker-php-ext-install mysqli
RUN docker-php-ext-install pdo
RUN docker-php-ext-install pdo_mysql
RUN docker-php-ext-install opcache

RUN apt-get install -y libssl-dev openssl
RUN docker-php-ext-install phar

RUN apt-get clean \
  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

이제 apache의 모드를 활성화하고 웹서버에 반영하는 부분을 추가한다.

# ...
RUN a2enmod rewrite
RUN a2enmod headers
RUN apache2ctl -k graceful

여기서 작성한 Dockerfile의 전체 내용은 다음과 같다.

FROM php:5.6-apache

RUN apt-get update
RUN apt-get install -y git zip

RUN apt-get install -y libpng12-dev libjpeg-dev
RUN apt-get install -y mysql-client
RUN docker-php-ext-configure gd --with-png-dir=/usr --with-jpeg-dir=/usr \
&& docker-php-ext-install gd

RUN docker-php-ext-install mbstring
RUN docker-php-ext-install mysqli
RUN docker-php-ext-install pdo
RUN docker-php-ext-install pdo_mysql
RUN docker-php-ext-install opcache

RUN apt-get install -y libssl-dev openssl
RUN docker-php-ext-install phar

RUN apt-get clean \
  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

RUN a2enmod rewrite
RUN a2enmod headers
RUN apache2ctl -k graceful

docker 실행하기 및 종료하기

docker-compose up을 실행한다. 처음 실행하면 이미지를 받고 빌드를 시작하는 것을 확인할 수 있다.

$ docker-compose up
Creating network "dockerhello_default" with the default driver
Pulling db (mariadb:5.5)...
5.5: Pulling from library/mariadb
4269eaa217cc: Downloading [=========>      ]  14.12MB/38.11MB
b5d5817a79f8: Download complete
5a270f0327f3: Download complete
911f94a14d77: Download complete
114588764b3b: Downloading [=============>  ]   5.12MB/5.994MB
d1dcaee5ec4a: Download complete

모든 빌드가 완료되면 현재 폴더에 있는 내용을 http://localhost/를 통해 접속할 수 있다.

앞서 phpmyadmin도 포함했고 포트에 연결했었다. 이제 http://localhost:8000/로 접속하면 phpmyadmin도 잘 실행되고 있는 것을 확인할 수 있다.

docker-compose down으로 종료한다.

docker 컨테이너 다루기

docker-compose로 실행한 후에 실행된 컨테이너를 보기 위해서는 docker ps 명령을 사용할 수 있다.

$ docker ps
CONTAINER ID        IMAGE                       COMMAND                  CREATED             STATUS              PORTS                            NAMES
bacdc4de6660        dockerhello_web             "docker-php-entrypoi…"   6 minutes ago       Up 5 minutes        0.0.0.0:80->80/tcp               dockerhello_web_1
408909957ec4        phpmyadmin/phpmyadmin:4.7   "/run.sh phpmyadmin"     6 minutes ago       Up 5 minutes        9000/tcp, 0.0.0.0:8000->80/tcp   dockerhello_phpmyadmin_1
4949e0a2d10f        mariadb:5.5                 "docker-entrypoint.s…"   6 minutes ago       Up 5 minutes        0.0.0.0:3306->3306/tcp           dockerhello_db_1

그 외에도 다양한 docker 명령어를 직접 사용해서 컨테이너를 제어할 수 있다. 다음 명령으로 컨테이너에 쉘을 실행하고 접속할 수 있다.

$ docker exec -it dockerhello_web_1 bash
root@bacdc4de6660:/var/www/html# 

다만 이렇게 접속해서 설정을 변경한다면 지금 당장은 문제가 없겠지만 다시 이미지를 생성했을 때는 그 변경 부분이 새 이미지에서는 적용되지 않는다. 도커파일을 사용하면 그런 반복되는 과정을 없에고 파일에 명시적으로 남기는 것으로 매번 동일한 환경을 구성할 수 있다. 설정을 추가하거나 변경할 필요가 있다면 그 명령어는 Dockerfile에 정의하자.


docker-compose를 처음 사용하고 난 후에 docker로 환경을 구성한다는 것이 더 와닿아서 일반적인 설명과는 조금 다른 순서로 적어봤다. 시작부터 대뜸 복잡하게 느껴졌다면 (죄송하지만) 위에 추천한 글을 읽어보자.

여기서는 웹서버가 포함된 php를 사용했지만 fpm을 사용하는 php로 분리하고 별도의 웹서버 컨테이너를 사용하는 것도 가능하다. 반대로 한 이미지에 lamp 스택을 모두 담고 있는 linode/lamp와 같은 경우도 존재한다. 개인적으로는 역할별로 컨테이너를 분리하기를 선호하는데 상황에 맞게 전략을 짜야겠다.

  • 이런 역할을 오케스트레이션(orchestration)이라고 한다. 
  • php에서는 callable 이라는 타입 힌트를 제공한다. 이 타입 힌트는 말 그대로 호출이 가능한 클래스, 메소드, 또는 함수인 경우에 사용할 수 있다. php에서는 타입이 별도의 타입으로 존재하지 않는 대신에 문자열로 처리하고 있어서 다소 모호한 부분도 있다. callable을 타입 힌트로 사용했을 때 어떤 값을 넘길 수 있는지 명확히 알고 있어야 한다.

    function callableOnly(callable $callable): void {
        // callable에 해당하면 다음처럼 호출할 수 있음
        call_user_func($callable);
    
        // 일부를 제외하고는 다음과 같이 호출 가능함
        $callable();
    }
    

    특히 callable은 명확한 제한 없이 열어두고 사용하면 보안 문제 등을 만들어낼 수 있기 때문에 유의해야 한다.

    callable

    다음은 callable에 해당하는 경우로 상당히 다양한 형태로 callable을 정의할 수 있다. 여기서는 callable인지 확인하는 is_callable() 함수를 사용했다.

    함수

    function sayHello() {
        echo 'Hello';
    }
    is_callable('sayHello'); // true
    

    꼭 사용자 정의 함수가 아니더라도 이와 같이 사용할 수 있다. 다만 언어에서 제공하는 구조는 callable에 해당하지 않는다. 예를 들면 isset(), empty(), list()는 callable이 아니다.

    is_callable('isset'); // false
    

    익명 함수

    $hello = function () {
        echo 'Hello';
    };
    
    is_callable($hello); // true
    

    정적 메소드

    class HelloWorld()
    {
        static function say()
        {
            echo 'Hello World!';
        }
    }
    is_callable('HelloWorld::say'); // true
    is_callable(['HelloWorld', 'say']); // true
    is_callable([HelloWorld::class, 'say']); // true
    

    ::class 상수는 PHP 5.5.0 이후로 사용할 수 있는데 해당 클래스의 네임스페이스를 포함한 전체 클래스명을 반환한다. 문자열로 사용하는 경우에는 개발도구에서 정적분석을 수행하지 못하기 때문에 오류를 검출하기 어렵다. 대신에 이 상수를 사용하면 현재 맥락에서 해당 클래스가 선언되어 있는지 없는지 검사해주기 때문에 이런 방식으로 많이 작성한다.

    주의할 점은 정적분석 기능이 없는 개발도구에서는 ::class를 사용해도 문자열을 사용하는 것과 차이가 없다. ::class는 실제로 해당 클래스로 인스턴스를 생성하거나 하지 않기 때문에 autoload와는 상관 없이 동작하기 때문이다.

    echo SomethingNotDefined::class; // "SomethingNotDefined"
    

    대신 런타임에서 is_callable을 사용하거나 callable로 넘겨주는 경우에 정적 메소드의 경우는 autoload를 통해 검사하는 식으로 동작한다.

    클래스 인스턴스 메소드

    class Person
    {
        protected $name;
    
        public function __construct(string $name)
        {
            $this->name = $name;
        }
    
        public function getName(): string
        {
            return $this->name;
        }
    }
    
    $edward = new Person('Edward');
    is_callable([$edward, 'getName']); // true
    

    클래스의 스코프 해결 연산자를 이용한 메소드

    스코프 해결 연산자(Scope Resolution Operator)를 callable에서도 사용할 수 있다. Paamayim Nekudotayim라고 부르는 ::를 의미한다.

    class Animal
    {
        public function getType()
        {
            echo 'Animal';
        }
    }
    
    class Dog extends Animal
    {
        public function getType()
        {
            echo 'Dog';
        }
    }
    
    $dog = new Dog;
    is_callable([$dog, 'parent::getType']); // true
    call_user_func([$dog, 'parent::getType']); // Animal
    
    $callable(); // 이 경우에는 이 방식으로 호출할 수 없음
    

    구현 메소드 대신 부모 클래스의 메소드를 직접 호출할 수 있다. 관계를 뒤집는 좋지 않은 구현이므로 이런 방식에 의존적인 코드는 작성하지 않는다.

    매직 메소드 __invoke()

    __invoke() 매직 메소드가 구현된 클래스는 인스턴스를 일반 함수처럼 호출할 수 있다.

    class PersonSay
    {
        protected $name;
    
        public function __construct(string $name)
        {
            $this->name = $name;
        }
    
        public function __invoke()
        {
            echo "Hello, {$this->name} said.";
        }
    }
    
    $say = new PersonSay('Edward');
    is_callable($say); // true
    call_user_func($say); // Hello, Edward said.
    

    익명 클래스의 경우도 호출 가능하다.

    $say = new class {
        public function __invoke(string $name)
        {
            echo "What up, {$name}?";
        }
    };
    is_callable($say); // true
    call_user_func($say, 'Edward'); // What up, Edward?
    

    이 매직 메소드는 손쉽게 상태를 만들어낼 수 있어서 유용할 때가 종종 있다.


    Iterator를 callable로 사용할 수 있을까?

    Iterator를 넘기면 인스턴스를 넘긴 것으로 인식해서 __invoke() 구현을 확인한다. 즉, Iterator를 루프를 돌려서 사용하지는 않는다.

    Closure vs callable vs 인터페이스

    매개변수로 익명함수만을 받고 싶다면 Closure를 지정할 수 있다. 하지만 익명함수에도 use 키워드로 스코프를 집어 넣거나 global로 전역 변수에 접근하는 방식도 여전히 가능하기 때문에 callable이 아니더라도 callable 만큼 열려 있는 것이나 마찬가지라는 점에 유의해야 한다. 열려있는 것 자체는 문제가 아니지만 Closure, callable은 전달받은 함수가 사용하기 적합한지 판단해야 하는 경우가 생긴다. 예를 들면 매개변수의 숫자라든지, 타입이라든지 사용 전에 확인해야 하는 경우가 있다.

    그래서 단순히 함수 기능이 필요하더라도 계약을 명확하게 들어내야 한다면 인터페이스를 활용하는게 바람직하다. 인터페이스를 사용하면 전통적인 방식대로 인터페이스를 구현해서 사용하면 되겠다. 물론 익명 클래스로 다음처럼 사용할 수 있다. 익명 함수에 비해서는 다소 장황하게 느껴질 수 있지만 사전조건으로 검증해야 하는 내용을 줄일 수 있다.

    interface NamedInterface
    {
        public function getName(): string;
    }
    
    function sayHello(NamedInterface $named): void {
        echo "Hello! {$named->getName()} said.";
    }
    
    sayHello(new class implements NamedInterface {
        public function getName(): string {
            return 'Edward';
        }
    });
    

    모든 방법에는 장단점이 있으므로 필요에 따라 어느 접근 방식을 택해야 할지 결정 해야겠다.

    Mike McQuaid의 Open Source Maintainers Owe You Nothing를 번역한 글이다. 이 번역은 CC-BY-NC-SA를 준수한다.


    오픈소스 메인테이너는 당신에게 빚진 적 없다

    이 포스트는 지난 10년간 오픈소스 커뮤니티에 참여한 경험과 8년간 Homebrew의 (현재 시점에서 누구보다도 가장 오래 참여한) 메인테이너로서 겪은 경험에 따라서 작성했다.

    오픈소스 소프트웨어 메인테이너에게 번아웃은 큰 문제다. 이 문제는 피할 수 있다. 메인테이너는 오픈소스 프로젝트를 즐겁고 건강하게, 오랜 기간동안 생산적으로 참여할 수 있다. 어떻게 가능할까? 바로 다른 메인테이너, 기여자, 또는 소프트웨어를 사용하는 사용자에게 의무감을 갖지 않는 것이다. 이 프로젝트가 개인적으로 이득을 보고 있는 상황도 상관 없다. (예를 들어 자기 홍보 가치가 있다거나 기부를 받고 있는 상황에도.)

    메인테이너가 의무를 갖지 않는다는 근거를 명시한 경우가 있는가? 실제로 존재한다. 오픈소스 라이센스 자체에 말이다. 먼저 깃헙에서 가장 인기있는 오픈소스 라이센스MIT 라이센스를 보자.

    이 소프트웨어는 상품성, 특정 목적 적합성, 그리고 비침해에 대한 보증을 포함한 어떠한 형태의 보증도 명시적이나 묵시적으로 설정되지 않은 “있는 그대로의” 상태로 제공된다. 소프트웨어를 개발한 프로그래머나 저작권자는 어떠한 경우에도 소프트웨어나 소프트웨어의 사용 등의 행위와 관련하여 일어나는 어떤 요구사항이나 손해 및 기타 책임에 대해 계약상, 불법행위 또는 기타 이유로 인한 책임을 지지 않는다.

    이 난해한 법률 용어를 평범한 문장으로 살펴보자. (참고로 나는 변호사가 아니다):

    • 지금 제공하는 소프트웨어는 전부 메인테이너가 제공한다는 점에 동의한 것이다 (즉, 버그를 포함한 모든 것을 뜻한다)
    • 메인테이너는 모든 사용자와 사용 사례에서 동작하는 것을 보장하지 않는다 (문서화되어 있더라도 말이다)
    • 메인테이너는 어떤 소프트웨어 사용에서든 발생하는 어떤 문제에도 법적인 책임이 없다. (당신이 입은 피해를 고치기 위해 돈을 지불하는 경우에도)
    • 소프트웨어를 어떤 방식으로든 사용하려면 위 내용에 대해 필수적으로 동의해야 한다.

    이 내용은 단순히 MIT 라이센스만 그런 것이 아니다. BSD 2-Clause, GPLv3, MPL v2.0, Apache v2.0, Unlicense 그리고 거의 대부분의 다른 오픈소스 라이센스에도 법적 책임에 대한 한계를 두고 어떤 보증도 명시적으로 제공하지 않는다. 요약하면, 어떤 오픈소스 소프트웨어를 사용했고 이 소프트웨어가 실서버의 모든 데이터를 지워버렸다고 해도 어느 누구에게도 소송을 걸 수 없다. 소프트웨어를 사용하는 것으로 어떤 부정적 결과도 개인의 책임이라고 동의했기 때문이다.

    하지만 실제로는 어떤 문제가 발생하면 메인테이너는 문제를 빠르게 해결해주기도 하고 마치 회사처럼 사과를 하기도 한다. 이 부분이 번아웃의 가장 큰 원인이다. 대다수의 오픈소스 소프트웨어는 자유 시간의 자원 봉사로 개발된다. 하지만 오픈소스 메인테이너와 사용자가 그 관계를 회사/고객 같은 관계로 생각한다면 이는 지속하기 어렵다.

    대다수의 메인테이너가 오픈소스 소프트웨어에 참여하기 시작한 이유는 재미도 있고 자신의 문제를 해결하고 있기 때문이다. 재미 대신 말도 안되는 의무만 지속되는 상황에서, 돈도 못버는데 지나치게 시간을 쏟고 재미 없는 일만 증가한다면 그건 메인테이너를 갈아넣는 일이다. 이런 상황에서 논쟁이 있을 만한 결정을 내리거나 그런 과정에서 모욕적인 얘기를 들으면 그 친구나 가족은 분명 “오픈 소스가 이런 고통을 감내할 만큼 가치있는 일이냐”고 질문할 것이다.

    이 문제를 어떻게 고칠까? 오픈소스 생태계에 있는 모두가 오픈소스의 법적 현실을 수용하는 것이 답이다. 이 답을 실제로 적용하면 이런 의미다.

    • 메인테이너: 프로젝트에서 더이상 즐거움을 느끼지 못한다면 더 이상 하지 않는다. 사용자가 프로젝트에 대해 이야기할 때 무례하게 군다면 접근을 막아버리고 그 사람들이 요청하는 것을 하지 않는다. 버그를 만들었다고 미안해 하지 않는다. 모든 소프트웨어에는 결함이 있기 마련이다. 세상에 무언가를 내놓고도 어떤 댓가도 바라지 않았다는 점에서 스스로를 자랑스러워 하자.
    • 기여자: 결정은 메인테이너가 내리도록 하고 모든 관련 기여 문서를 읽었는지 확인한다. 메인테이너는 프로젝트를 운영하는 사람이고 궁극적으로 그들의 단어가 남게 된다. 어떤 식으로 프로젝트가 동작하는지 (사실, 무엇이든지) 당신에게 가르치기 위해서 메인테이너가 존재하는 것이 아니다.
    • 사용자: 프로젝트에 이슈를 생성하거나, 풀리퀘스트를 열거나 덧글을 작성할 때는 사람들이 자신들의 시간을 써서 소프트웨어를 만들고 무료로 사용하게 했다는 점에 감사하는 마음을 담아야 한다. 불만이든 어떻게 할 수 없는 부정적인 것들은 본인의 몫이다. (적어도 보이는 곳에서 말하지 말아야 한다.) 본인 스스로에게 더 시간을 들여 문제를 풀지 않으면서 다른 사람에게 물어서 해결하려고만 한다면 다른 사람이 그 문제를 해결해줄 거라고 기대하지 말아라. 즉, 어떤 도움이든 물어보기 전에 모든 문서를 읽어보고 스스로 문제를 해결하려고 노력하라는 의미다.

    이런 역할을 모두 지킨다면, 메인테이너가 실제로 있지도 않은 의무에 치여서 프로젝트가 접히는 경우가 더 적어질 것이다. 오픈소스 생태계에서 본인이 어떤 역할로 참여하고 있는지 잘 인지한다면 더 행복한 메인테이너, 기꺼이 돕는 기여자와 감사하는 사용자가 될 수 있을 것이다.

    최근 emacs에 대한 유튜브를 보는데 전혀 IT쪽 일을 하지 않는 사람도 org-mode 때문에 emacs를 사용한다는 얘기를 듣고 문서를 찾아보게 되었다.

    emacs는 아예 처음 사용해보는데 이 문서에 emacs 기능도 간략하게 설명하고 있어서 org-mode의 기능을 살펴보는데 불편함이 없었다. 각각 기능도 강력하고 일반 텍스트를 이렇게 멋지게 조작 가능한게 놀랍다. markdown이나 html로 내보내는 기능도 멋지다. 내보내는 포맷은 플러그인으로 추가도 가능하다. 익숙해지면 정말 유용할 것 같다.

    이 번역 문서는 GNU 자유 문서 사용 허가서 버전 1.3 또는 그 이후의 버전이 적용된다. 코드 예제는 GNU GPL 버전 3 또는 이후 버전이 적용된다. 원본은 Org mode beginning at the basics 페이지에서 찾을 수 있다.


    Org-mode 기초부터 시작하기

    Org-mode는 공식 웹페이지에서 설명하는 것과 같이 노트와 할 일 목록을 관리하고 프로젝트 계획을 작성하거나 저작하는데 사용할 수 있는, 빠르고 효과적인 플레인 텍스트 시스템입니다. Emacs 22.2와 XEmacs 22.1부터 지원하기 시작했습니다. 아래 내용은 간단한 튜토리얼로 Emacs와 org-mode를 사용하는 방법을 설명합니다.

    Emacs에 대해 알아야 할, 정말 최소한의 지식

    무엇이든지 하고싶은 일을 하기 위해서 Emacs에서 필수적으로 알아야 할 최소 지식은 다른 애플리케이션에서 알아야 하는 양보다 많습니다. 하지만 이런 비교는 일반적인 장난감과 레고를 비교하는 것과 같습니다. 레고는 시작하기 어렵지만 (작은 플라스틱 조각이 가득한 상자에서 시작합니다) 장기적으로 봤을 때는 더 많은 것을 만들 수 있습니다.

    Emacs는 단축키가 풍부합니다. 처음 시작할 때는 이런 특징에 짜증날지 모르지만 시간이 흐를수록 마우스를 점점 적게 사용하게 되고 실제로 더 빠르게 작업을 할 수 있게 될겁니다.

    기본적인 모든 동작은 마우스를 사용할 수 있습니다. 파일을 열거나 저장하는 등의 작업은 메뉴에서 모두 가능한 동작입니다. 하지만 키보드에서 손을 때고 마우스를 잡는 방법보다 단축키를 사용하는 방식이 훨씬 빠르다는 것을 알게될 것입니다.

    Emacs는 이중 단축키를 많이 사용합니다. 대부분의 애플리케이션처럼 Alt-F 나 Alt-S 대신에 Control-X Control-FControl-X Control-S 를 사용합니다. 처음에는 생산성에 반하는 것처럼 느껴질지 몰라도 금방 익숙해질 겁니다.

    노트: 키 축약 표현

    • M – Alt (고대 키보드에서는 Meta 키였습니다.)
    • C – Control
    • S – Shift
    • C-x f – 이 표기는 Control x를 누른 후, 둘 다 손을 땐 다음에 f를 누른다는 의미입니다.

    어느 버전의 Emacs를 사용해야 하나요?

    어느 버전이든 다 똑같이 느껴진다면 XEmacs보다 Emacs를 선택하기 바랍니다. (이 말에 동의하지 못한다면 이미 이 문장을 넘겨도 될 만큼 알고 있다는 의미입니다.) 다음 링크가 도움이 될 겁니다.

    리눅스에서는 패키지 매니저를 사용해서 Emacs를 설치합니다. 데비안이라면 다음처럼 설치합니다.

    sudo apt-get install emacs

    설정하기

    Emacs를 시작할 때 가장 큰 고통은 바로 설정하기에 있습니다. 설정을 위한 메뉴도 없고 텍스트 파일을 수정해야 합니다. (메뉴가 있다고 얘기하긴 하는데 그건 그냥 순수한 사람들을 낚는 겁니다.) 설정 파일의 위치는 (심지어 이름까지도) 어떤 운영체제를 사용하느냐에 따라 다릅니다. 하지만 플랫폼에 상관없이 그 내용은 거의 일치합니다. 대다수 사람들은 동일한 설정 파일을 다른 운영체제서 사용합니다. 장기적으로 보면 최고의 선택이나 마찬가지죠!

    설정 파일의 위치는 다음과 같습니다.

    org-mode 시작하기

    이 챕터에서 사용하는 새로운 단축키는 다음과 같습니다.

    • C-x s – 문서 저장하기
    • C-x f – 문서 열기

    첫 org-mode 문서

    이제 첫 org-mode 문서를 시작하기 위해서 알아야 할 모든 지식을 습득했습니다. Emacs를 시작합니다. 완전히 새로 설치한 Emacs라면 Emacs의 스플래시 화면을 볼 수 있을 겁니다. 이 화면에는 Emacs 튜토리얼과 여러 문서를 볼 수 있는 바로가기가 있지만 지금은 건너뛰고 다음으로 넘어갑니다.

    C-x f 단축키를 사용해서 새 문서를 시작합니다. 이 단축키는 문서를 열기 위한 기능을 제공합니다. (Emacs에서는 버퍼라고 말합니다.) 여기에 *1.org*라고 입력합니다. 이제 새로운 빈 문서가 화면에 나타납니다.

    이 문서를 저장하기 위해서 저장 아이콘을 누르거나 C-x s 단축키를 누르고 1.org를 입력합니다.

    Emacs는 org-mode 문서를 편집하려고 한다는 점을 아직 이해하지 못합니다. 현재 문서에서 org-mode를 활성화하려면 다음 명령을 입력합니다.

    M-x org-mode

    이 명령으로 현재 문서에서 org-mode를 활성화 할 수 있습니다.

    Emacs가 org-mode 문서를 인식하도록 다음 내용을 문서 상단 에 추가합니다.

    MY PROJECT -*- mode: org -*-

    여기에 사용한건 뺄셈 기호며 밑줄이 아닙니다. MY PROJECT는 문서의 제목이며 마음대로 지정할 수 있습니다.

    이 한 줄의 내용이 이 문서에서 org-mode를 활성화합니다. 이 내용을 넣으면 파일 확장자와 상관없이 org-mode가 동작합니다.

    org 파일에서 항상 org-mode를 활성화하려면 Emacs 설정을 수정해야 합니다. 수정하는 방법은 다음 섹션에서 설명합니다.

    Emacs 설정 처음으로 수정하기

    Emacs 설정 파일을 엽니다. Emacs에서 이 파일을 열기 위해서 C-x f 를 사용합니다. (설정 파일의 경로는 설정을 참고합니다.) 그리고 다음 내용을 추가합니다.

    ;; -*- mode: elisp -*-
    
    ;;스플래시 화면을 끔 (다시 켜려면 t를 0으로 변경)
    (setq inhibit-splash-screen t)
    
    ;;문법 강조를 활성화
    (global-font-lock-mode t)
    (transient-mark-mode 1)
    
    ;;;;org-mode 설정
    ;;org-mode 활성화
    (require 'org)
    ;;org-mode를 .org로 끝나는 파일에서 활성화
    (add-to-list 'auto-mode-alist '("\\.org$" . org-mode))
    

    Emacs를 재시작합니다.

    노트: 앞에서 추가했던 mode 행은 1) Emacs의 설정에 지정한 org-mode 확장자와 다른 확장자를 사용하는 경우 (예로 myfile.txt) 2) auto-mode-alist 행을 설정에 추가하지 않은 경우에만 필요합니다.

    목록과 노트 관리하기

    이 장에서 사용하는 단축키입니다.

    • TAB / S-TAB – 접기/펴기
    • M-up/down – 제목행 위/아래로 이동하기
    • M-left/right – 제목행 수준 높이기/낮추기
    • M-RET – 새 제목행 추가하기
    • C-x s – 파일 저장하기
    • C-h t – Emacs 튜토리얼

    이제 org-mode 문서를 사용할 수 있도록 설정한 Emacs가 있으니 이제 시작하기만 하면 됩니다. org-mode를 시작하는데 도움이 될 개요를 적는 것으로 시작합니다. 새 문서를 열고 (C-x b) 2.org를 입력한 다음에 아래 내용을 복사해서 붙여넣습니다.

    #-*- mode: org -*-
    #+STARTUP: showall
    
    * org-mode 시작을 환영합니다
      Org-mode에 오신 것을 환영하고 감사드립니다. org에서 개요를 작성하기는 매우 간편합니다.
      그냥 텍스트거든요! 그저 입력하면 됩니다.
    
    * 제목행은 하나 이상의 별 문자로 시작합니다.
      제목은 별 하나, 부제목은 별 두 개 방식으로 숫자를 늘려갑니다.
    
    * 목록 작성하기
    ** 개요 이동하기
    ** 제목행 이동하기
    

    파일을 2.org로 저장합니다. (C-x s) 저장하면 문법 강조가 켜져있어서 색상이 변경되는 것을 확인할 수 있습니다. Emacs가 org-mode에서 작업중인 것을 알고 있기 때문입니다.

    이제 정말로 org-mode를 시작할 준비가 되었습니다!

    목록 작업하기

    목록은 브레인스토밍과 모든 항목을 관리하는데 뛰어난 방식입니다. 목록을 사용하면 기록을 하는 동안 큰 그림 그리기에 집중할 수 있도록 돕습니다.

    가장 먼저 할 일은 접기입니다. 특히 문서가 길어질 때는 이 기능이 유용합니다. 예제 문서에서 첫 제목행인 이동합니다. (방향키를 사용하세요.) org-mode 시작을 환영합니다 에 커서를 위치한 후에 TAB 을 눌러봅니다. 그리고 S-TAB 도 사용해봅니다. Tab 은 현재 부분을 접거나 펼 수 있습니다. 시프트 키와 함께 누르면 전체 문서를 접고 펼 수 있습니다.

    브레인스토밍의 기본적인 아이디어는 항목을 목록으로 적는 것입니다. 적은 후에 항목의 순서를 중요도 순서로 다시 정렬하고 싶을 것입니다. 제목행을 위로, 또는 아래로 이동하기 위해서 제목행에서 M-up/down 을 사용할 수 있습니다. 목록을 모두 접어서 제목만 보이는 상태는 전체 개요를 파악하는데 도움됩니다. 동시에 세부적인 내용도 잃어버리지 않고 잘 보존하고 있습니다.

    다음으로 제목 계층을 올리거나 낮출 수 있습니다. 예를 들어 제목행은 하나 이상의 별 문자로 시작합니다. 행을 목록 작성하기 의 부제로 만들고 싶다면 제목의 위치를 아래로 이동한 후에 M-right 으로 계층을 낮출 수 있습니다.

    마지막으로 새 제목행을 추가하기 위해서는 M-RET 을 사용합니다.

    목록은 순서 없는 목록과 순서 있는 목록이 있습니다. 다음을 확인하세요.

    ** 반지의 제왕
       가장 좋아하는 장면 (순서대로)
       1. 로히림 전투
       2. 에오윈과 마술사왕의 싸움
          + 이 내용은 이미 책에서도 가장 좋아하는 장면
          + 미란다 오토를 정말 좋아함
       3. 레골라스에게 화살 맞는 피터 잭슨
          - DVD에만 있음
          화살 맞을 때 정말 웃긴 표정이었음.
       하지만 결과적으로 각각의 장면이 아니라 영화 전체가 좋았다.
       영화에서 중요했던 배우:
       - 일라이저 우드 :: 프로도 역
       - 숀 애스틴 :: 샘 역, 프로도 친구로 나옴. 여전히 그를 구니스에서 나온 미키 월쉬로
         잘 기억하고 있음.
    

    순서 없는 목록은 -, + 또는 \*로 시작합니다. 순서 있는 목록은 숫자와 점으로 시작합니다. 설명은 ::이 붙습니다.

    더 보기: 이 스크린캐스트 에서는 일반 목록의 몇 가지 기능을 설명합니다. 이 내용은 메뉴얼 에서도 볼 수 있습니다.

    기록하기

    내용을 작성할 때 쓸 수 있는 몇 가지 표준적인 마크업이 있습니다. 다음과 같은 마크업을 사용할 수 있습니다.

    단어에 *굵게*, /기울임꼴/, _밑줄_, =코드=, ~요약~ 등의 마크업을 쓸 수 있다. 꼭 필요한 경우 +삭제선+도 가능하다.

    다음처럼 표현됩니다.

    단어에 굵게, 기울임꼴, 밑줄, 코드, 요약 등의 마크업을 쓸 수 있다. 꼭 필요한 경우 삭제선 도 가능하다.

    여기까지 봤다면 Emacs 튜토리얼을 살펴보는 것도 좋습니다. C-h t 로 Emacs에 내장되어 있는 튜토리얼을실행할 수 있습니다. 튜토리얼은 몇 가지 Emacs 단축키와 함께 어떻게 문서를 이동하는지 알려줄 것입니다.

    할 일 항목 사용하기

    이 챕터에서 사용하는 새 단축키입니다.

    • S-left/right – 워크플로를 변경
    • C-c C-v – 현재 문서에 있는 할 일 목록 보기

    기본 할 일 기능

    org-mode를 사용하는 가장 큰 이유는 할 일을 관리하는데 사용하기 위해서 입니다. 먼저 할 일 기능을 사용하기 위해서는 특별히 해야 하는 작업 없이 TODO 키워드를 제목행에 추가하면 됩니다.

    ** TODO 비행기 구입하기
    

    할일 목록을 빠르게 사용하려면 다음 단축키를 사용합니다.

    • S-left/right

    이 단축키는 TODO 에서 DONE 까지, 그리고 빈 칸을 순환하며 상태를 변경합니다.

    큰 문서를 작업하고 있고 문서 곳곳에 할 일 목록이 흩어져 있는 상황에서는 C-c / t 로 현재 할 일 항목만 남기고 나머지는 모두 접을 수 있습니다.

    할 일 설정하기

    • 파일 내에서 설정하기

      Org-mode 파일에서는 워크플로 상태를 파일 시작 위치에서 설정할 수 있습니다. 다음과 같이 파일 시작부에 작성합니다.

      #+TODO: TODO IN-PROGRESS WAITING DONE
      

      이 행은 파일 상단에 위치해야 하며 상단과 #+TODO 행 사이에 빈 행이 없어야 합니다.

      새 워크플로를 활성화하기 위해서는 파일을 새로 열거나 파일 최상단에 #로 시작하는 행으로 이동한 다음에 C-c C-c 를 입력합니다.

      워크플로를 복사해서 테스트 파일인 1.org에 붙여놓고 차이를 확인해봅시다.

    • Emacs 설정 파일에서

      워크플로 상태를 모든 org 파일에 직접 추가하는 방법은 번거롭습니다. 어디서나 사용할 수 있도록 설정하려면 설정 파일에 추가합니다. 다음 내용은 (require 'org) 행 이후에 추가합니다.

      (setq org-todo-keywords
        '((sequence "TODO" "IN-PROGRESS" "WAITING" "DONE")))
      

      워크플로 상태를 활성화하기 위해서 Emacs를 재시작합니다.

    아젠다

    이 장에서 사용하는 단축키입니다.

    • C-c a – 아젠다
    • C-c [ – 아젠다 파일 목록에 문서를 추가
    • C-c ] – 아젠다 파일 목록에서 문서를 제거
    • C-c . – 일자 추가
    • C-u C-c . – 일자와 시각 추가
    • C-g – 하던 일을 멈추고 벗어남

    아젠다(agenda)라는 단어의 기본적인 의미는 완료 해야 할 것 으로 라틴어인 agendum 에서 왔습니다. Org-mode는 다양한 종류의 아젠다, 일감 목록을 만들고 하나 또는 여러 org 문서에서 이런 일감을 수집하기 매우 좋습니다.

    활성화된 모든 일감 목록 생성하기

    1.org를 기본 아젠다 파일로 사용하고 나중에 Emacs의 설정 파일에서 어떻게 동작하는지 살펴보겠습니다.

    1.org를 엽니다. C-c a 를 눌러서 아젠다를 엽니다. 아젠다는 다음처럼 나옵니다.

    Press key for an agenda command
    -------------------------------
    a Agenda for the current week or day
    t List of all TODO entries

    위 내용은 일부 내용이고 실제로는 더 많은 내용이 출력될 것입니다.

    아쉽게도 위 두 항목은 빈 목록을 보여줄 것입니다. (직접 눌러서 확인해볼 수 있습니다.) 그러므로 이 상태에서 C-g 를 눌러 빠져 나옵니다. 이제 1.org를 아젠다 파일로 추가하기 위해서 C-c [ 를 사용합니다. 이제 아젠다 메뉴로 가서 (C-c a) t 를 누르면 모든 할 일 항목 목록을 확인합니다.

    할 일 항목 사용하기에서 설명한 방식대로 더 적절한 워크플로를 추가하는 과정을 했다면 DONE을 제외한 모든 항목이 모두 나타나는 것을 볼 수 있습니다.

    이 과정은 여러 문서에서 반복할 수 있습니다. 아젠다는 할 일 전체 목록을 제공할 것입니다. 만약 문서를 아젠다 파일 목록에서 빼고 싶다면 C-c ] 를 사용하면 됩니다.

    약속과 마감 일시

    일반적으로 시간 설정이 필요한 일감은 달력에 표시합니다. org-mode는 이 방식도 지원합니다. 아젠다는 모든 할 일을 시간 기반 목록으로 볼 수 있습니다. 다음 내용을 참고하기 바랍니다.

    1.org에 새 (부)제목을 프레드에게 전화하기 라고 추가합니다. (M-RET프레드에게 전화하기) 다 입력한 후에 C-c . 를 입력합니다. 이 명령을 입력하면 화면 밑에 일자 선택지가 표시됩니다. 직접 손으로 입력할 수도 있고 S-left/right 으로 선택할 일자를 변경할 수 있습니다. 만약 일자 외에 시간도 추가하고 싶다면 C-c . 대신에 C-u C-c . 를 입력합니다.

    이제 아젠다 (C-c a)로 이동해서 a 를 누르면 아젠다 항목을 확인할 수 있습니다.

    더 읽을 거리:

    Emacs 설정 파일에서 아젠다 설정하기

    C-c [ 을 사용해서 아젠다 목록에 추가한 후에 Emacs의 설정 파일을 확인하면 다음 내용을확인할 수 있습니다.

    (custom-set-variables
      ;; custom-set-variables was added by Custom.
      ;; If you edit it by hand, you could mess it up, so be careful.
      ;; Your init file should contain only one such instance.
      ;; If there is more than one, they won't work right.
     '(org-agenda-files (quote ("~/Documents/Projects/org4beginners/2.org"
     "~/Documents/Projects/org4beginners/1.org"))))
    (custom-set-faces
      ;; custom-set-faces was added by Custom.
      ;; If you edit it by hand, you could mess it up, so be careful.
      ;; Your init file should contain only one such instance.
      ;; If there is more than one, they won't work right.
     )
    

    Emacs lisp의 세계에 오신 것을 환영합니다. Emacs가 설정 파일을 변경한 경우에 이런 방식으로 기록됩니다. (참고: Aquamacs 에서는 customizations.el이라는 별도 파일에 저장됩니다.)

    여기서 중요한 내용은 중간에 있는데 (5, 6행) org-agenda-files 내용을 확인할 수 있습니다. 아젠다 목록을 만들기 위해 사용하는 아젠다 파일의 목록이 여기에 지정되어 있습니다. 지금은 일단 그대로 둡니다. 다음에 설정 파일을 살펴볼 일이 있다면 적어도 이게 무슨 기능을 하는지 알 수 있을 것입니다.

    더 읽을 거리: 사용자 정의 아젠다 명령

    GTD

    이 챕터에서 사용하는 단축키입니다.

    • C-c C-c – 태그 추가

    Getting things done 은 유명한 시간 관리 방법 중 하나로 구글에서 검색하면 1억 5천 여 항목에 이릅니다. 태그를 사용하면 org mode에서도 비슷한 방식으로 할 일 관리를 할 수 있습니다.

    태그는 다른 종류의 할일 목록을 조직화하는데 사용합니다. 예를 들면 전화 일정, 읽을 책 목록, 장바구니 목록을 묶기 위해 씁니다.

    태그를 추가하기 위해서 다음 설정을 문서 상단에 추가합니다.

    #+TAGS: { @OFFICE(o) @HOME(h) } COMPUTER(c) PHONE(p) READING(r)

    문서를 다시 불러오거나 #으로 시작하는 곳에서 C-c C-c 를 입력합니다.

    이제 문서 어느 행에서든 하나 또는 그 이상의 태그를 등록할 수 있습니다. C-c C-c 를 누르면 다음과 같은 팝업이 나타납니다.

    Inherited:
    Current:
    { [o] @OFFICE     [h] @HOME    }
      [C] COMPUTER   [p] PHONE   [r] READING
    

    문서 서두에 정의한 태그를 사용할 수 있는 바로가기입니다. 첫 두 태그는 (OFFICE와 HOME)은 상호배타적이라 둘 중 하나만 선택할 수 있지만 나머지는 자유롭게 추가할 수 있습니다.

    GTD 설정의 좋은 예제로는 이 글을 참고하세요: Emacs에 Org-mode를 사용해서 GTD 구현하기

    Emacs 설정 파일에 태그 추가하기

    Emacs 설정 파일에 태그를 추가하려면 다음처럼 설정에 입력합니다.

    (setq org-tag-alist '(("@work" . ?w) ("@home" . ?h) ("laptop" . ?l)))

    설정 파일에서 상호배타적인 태그 그룹을 만들기 위해서는 메뉴얼 내용을 참고합니다.

    이 설정은 문서 상단에 추가한 내용으로 덮어쓰는 것이 가능합니다. 그래서 각 문서에 개별적인 워크플로와 태그를 직접 설정해서 사용할 수 있습니다.

    태그를 활발히 활용하는 예제는 여기서 확인할 수 있습니다.

    내보내기

    여기서 사용하는 단축키는 다음과 같습니다.

    • C-c C-e – 내보내기 메뉴

    org-mode에서만 문서 작업을 한다면 큰 문제가 없습니다. 하지만 간혹 다른 포맷으로 문서를 내보내야 할 필요가 있습니다.

    예를 들어 현재 문서를 내보내는데 html로 내보내려고 합니다. C-c C-e 를 누른 후에 h o 를 순서대로 누릅니다. 이 방법은 문서를 html로 내보낸 후, 그 내보낸 파일을 브라우저로 열게 됩니다.

    더 읽을 거리: html 출판 튜토리얼을 보면 여기서 설명한 내용보다 더 상세하게 다룹니다. 이 방법으로 완전한 웹사이트를 출판하는 것도 가능합니다. 그리고 메뉴얼에서 html, latex, pdf 그리고 다른 포맷으로 내보내는 방법을 설명합니다.

    org-mode에 능숙해지기

    효율적인 도구를 사용해서 시간을 아끼려면 그 도구를 잘 알아야 합니다. org-mode를 잘 알기 위해서는 메뉴얼을 읽고 사용하는게 중요합니다. Org-mode는 문서화가 잘 되어 있습니다. 가장 빠르게 org-mode 문서를 Emacs에서 읽는 방법으로는 info browser를 사용할 수 있습니다.

    이 창을 호출하기 위해서는 C-h i를 입력합니다. 그리고 링크 간 이동하기 위해서 TAB 키를 사용합니다.

    info-browser를 이동할 때는 다음 키를 쓸 수 있습니다.

    • u – 위로(up)
    • n – 다음(next)
    • p – 이전(previous)

    org-mode 메뉴얼 다음으로는 worg 웹사이트가 있습니다. 여러 재밌는 아이디어와 튜토리얼을 찾을 수 있습니다.

    기능을 빠르게 참고할 수 있는 org-mode 치트시트와 emacs 치트시트가 있습니다. 이 두 문서는 귀찮은 단축키를 기억하는데 도움이 될 것입니다.

    기초를 넘어서

    Geek 유머에 "여기에 용이 있어요!"가 있습니다. 여기서부터는 org-mode를 자유롭지만 스스로 책임져야 하는 사용법을 설명합니다. 대부분 다음 내용은 실제로 정말 어렵거나 한 것은 아니지만 적어도 중요한 데이터는 백업을 해두시기 바랍니다. 만약 다음 내용에서 궁금한 부분이 있다면 메뉴얼과 질문 답변을 확인합니다. 또한 IRC (freenode의 #orgmode)에서 질문하는 것도 좋은 방법입니다.

    TODO Quickly adding tasks with remember

    (역주: 아직 내용이 존재하지 않는 항목입니다.)

    최신 버전 org-mode 사용하기

    여기서 사용하는 명령입니다.

    • M-x org-reload – 업데이트 후 org-mode를 다시 불러오기
    • M-x org-version – org-mode 버전 확인하기

    Emacs가 업데이트 되는 속도보다 org-mode가 더 빠르게 개발되는 것을 아마 알 수 있을겁니다. 게다가 매일 org-mode의 개발 버전을 받아서 사용하는 것도 가능합니다.

    어떻게 가능할까요?

    1. git 설치하기 org-mode 튜토리얼에서 다룰 부분은 아니지만 다음 내용을 참고하세요.
      sudo apt-get install git
    2. org-mode의 코드를 어디에 저장할지 결정합니다. 여기서는 ~/Build/Emacs/org-mode 에 저장하지만 어디 하나 별 차이가 없으니 편한 위치에 저장하기 바랍니다.
    3. org-mode의 최신 버전을 받습니다.
      mkdir ~/Build/Emacs
      cd ~/Build/Emacs
      git clone git://orgmode.org/org-mode.git
      cd org-mode && make && make doc
      
    4. Emacs-init 파일에 다음 내용을 추가합니다.
      (setq load-path (cons "~/Build/Emacs/org-mode/lisp" load-path))
      (setq load-path (cons "~/Build/Emacs/org-mode/contrib/lisp"
      load-path))
      

      (require 'org-install)

      중요! 일반 버전의 org-mode라면 다음을 사용합니다.

      :(require 'org) 다음을 사용한다면 위 행은 반드시 제거해야 합니다.

      :(require 'org-install)

    5. 최신 org-mode를 유지하려면 다음 명령을 사용합니다.
      cd ~/Build/Emacs/org-mode
      git pull && make clean && make && make doc
      
    6. org-mode를 다시 불러옵니다. M-x org-reload 또는 Emacs를 재시작하세요.

    어떤 버전의 org-mode를 사용하고 있는지 확인하려면 M-x org-version 을 입력합니다.

    나도 신입으로 일을 시작했을 때 혼자 인터넷 검색창을 붙들고 코드와 씨름한 경험이 있었다. 작은 회사에서 유일한 개발자라 물어볼 선임도 없었고, 문제는 어떻게든 기간 내에 해결해야 하는 상황이 많았다. 모두가 신입 시절을 거치는 동안 그런 벽을 마주할 때가 있을 것 같다. 넘어야 할 산은 높고 나는 너무나 작게만 느껴지는 그런 경험을 거치고서 각자 지금의 자리에 있지 않나, 생각한다. <바쁜 팀장님 대신 알려주는 신입 PHP 개발자 안내서>라는 책 제목을 보는 순간에 그 신입 당시의 기억이 먼저 떠올랐다.

    바쁜 팀장님 대신 알려주는 신입 PHP 개발자 안내서 표지

    개발을 코드를 작성해서 실행하는 일이라고 단순하게 설명할 수 있지만, 현실에서는 그렇게 간단하지 않다. 코드를 어떻게 잘 작성하는가도 중요한 주제지만 그만큼 코드를 잘 관리하는 일도 중요하다. 게다가 코드가 실행되는 환경을 이해하는 과정도 필요하다. 환경도 여러 계층에 걸쳐 있다면 두루두루 살펴봐야 한다. 언어도, 환경도 서로 쉽게 이해하고 공유할 수 있도록 다양한 규칙과 규약을 정리해놓고 있고 이런 부분도 숙지해야 한다. 개발이라고 말하기에는 다각적으로 알아야 하는 부분이 많다.

    신입으로 첫날 출근하면 이런 지식에 압도당한다. 신입으로 시작할 때 가장 막막한 점은 단순히 언어에 대한 이해를 넘어서 이런 다양한 지식을 한꺼번에 흡수해야 한다는 점이다. 순식간에 넓은 분야를 깊이 있게 이해하는 일은 생각만으로도 벅차다. 알아야 할 모든 내용을 단번에 이해하면 좋겠지만 절대 쉬운 일이 아니다. 그런 막막한 상황에서 가장 중요한 것은 알아야 할 내용의 키워드를 습득하는 것이다. 키워드를 알면 그 키워드를 중심으로 아는 범위를 쉽게 늘릴 수 있다. 그런 점에서 이 책은 폭 넓으면서도 중요한 키워드를 모두 포함하고 있어서 학습에 좋은 가이드가 된다.

    일단 PHP를 기준으로 설명하기 때문에 PHP를 사용하는 사람이라면 꼭 알아야 할 내용을 잘 다루고 있다. 그리고 많은 내용을 PHP에 할애하고 있긴 하지만 거기에 덧붙여 지금 시대에 웹개발을 한다면 필수적으로 알아야 할 다양한 키워드를 장마다 풀고 있다. 또한, 각 문제와 주제에 대해 어떤 식으로 접근해야 하는지 복잡하지 않게 설명한다. 실제로 문제를 마주하게 될 때 어떤 식으로 생각하고 찾아봐야 하는지 그 방법도 잘 전달하고 있다.

    • 저장소가 뭔가요? (버전 관리 시스템)
    • 저장소의 소스코드를 받았는데 왜 안되죠? (컴포저)
    • 제 컴퓨터에서는 잘 되는데요? (가상 머신을 이용한 개발 환경 구축)
    • 어떤 파일을 고쳐야 할 지 모르겠어요 (프런트 컨트롤러 패턴과 MVC 패턴)
    • GET, POST는 알겠는데 PUT, DELETE는 뭔가요? (HTTP와 REST)
    • 그렇게까지 해야 하나요? (시큐어 코딩)
    • 그냥 제 스타일대로 하면 안되나요? (코딩 컨벤션과 PHP 표준 권고)
    • MySQLi는 나쁜건가요? (PDO와 ORM)
    • 메모장에 코딩하면 안되나요? (통합 개발 환경)

    책을 읽기 시작하자마자 끝까지 고개를 끄덕이며 읽었다. 내가 신입으로 들어갔을 때 이런 책이 있었으면 얼마나 수월하게 배우기 시작했을까. 주변에 PHP를 사용하는 사람이 있다면 꼭 알려주자. 신입 PHP 개발자에게는 어떻게 시작해야 하는지 좋은 가이드가 되고 선임이나 팀장급 이상이라면 어떤 내용을 신입에게 가르쳐야 하는지 명확한 지침이 되는 책이다.

    최근 php로 cli 도구를 만드는 프로젝트를 했다. 타겟 서버에 접속하는 권한이 제한적이고 나도 프로젝트 스코프 내에서만 접근 가능한 상황이라 ci를 돌리기 좀 애매해서 bash로만 작성해뒀다. 총 12개의 물리 서버에 도구를 배포하는데 배포가 완료되면 notification을 띄우고 싶어서 찾아봤다.

    특별한건 아니지만 그냥 보기 좋으니까. ?

    osascript 사용하기

    가장 간단한 방법이다. osascript로 노티를 띄울 수 있다. 장점은 1줄이면 된다는 점이다. 단점은 클릭하면 script editor가 열리는 점, 아이콘 변경이 안되는 점이다. bash에서 스크립트를 다음처럼 실행하면 된다.

    osascript -e 'display notification "노티 내용" with title "타이틀" sound name "Basso"'
    

    소리명은 ~/Library/Sounds, /System/Library/Sounds에서 찾을 수 있다.

    terminal-notifier 사용하기

    terminal-notifier를 설치해서 쓰는 방법이다. 아이콘이라든지 마음대로 다 변경할 수 있지만 따로 설치해야 한다. homebrew로 설치 가능하다.

    terminal-notifier -title "Hello" -subtitle "코드 배포" -message "배포가 완료되었습니다" -appIcon https://haruair.com/logo.png
    

    node-notifier 사용하기

    node로 작성되어 있다면 node-notifier를 사용하면 된다. 멀티플랫폼을 지원한다. mac은 terminal-notifier를 포함해서 배포하기 때문에 terminal-notifier의 기능을 전부 사용할 수 있고 js에서 간단하게 불러낼 수 있다.

    const notifier = require('node-notifier');
    
    notifier.notify({
        title: "Hello",
        message: "Hello World!",
    });
    

    리액트의 Advanced guides 페이지를 따라하면서 노트한 내용이다. 가이드 쪽은 옴니버스 같은 기분이라서 반반으로 나눠 읽기로 했다. 기록하고 싶은 부분만 남겼기 때문에 자세한 내용은 각 페이지를 참고한다.

    Reconciliation

    React는 선언형 API를 사용하고 있어서 변경에 대해 일일이 신경쓰지 않아도 된다. 이 가이드에서는 React가 어떤 비교 알고리즘을 사용해서 고성능을 내는지 설명한다.

    모든 컴포넌트를 다 새로 그리면 O(n3)인데 다음 두 가정으로 발견적(휴리스틱, heuristic) O(n) 알고리즘을 사용한다. 대부분의 경우는 이 가정에 문제가 없다.

    1. 다른 타입의 두 엘리먼트는 다른 트리를 만듬
    2. 개발자가 key로 힌트를 제공해서 자식 엘리먼트가 반복되는 렌더링에서 안정된 상태인걸 확인할 수 있음

    엘리먼트가 갱신될 때, 어떤 식으로 갱신이 발생하는가는 다음과 같다.

    • 다른 타입의 엘리먼트인 경우, 트리 전체를 다시 그림 (언마운트 && 마운트 발생)
    • 동일 타입의 DOM 엘리먼트의 경우, 어트리뷰트만 갱신함, 어트리뷰트 일부만 갱신된 경우 변경된 부분만 갱신 (e.g. stylefontWeight)
    • 동일 타입의 컴포넌트 엘리먼트의 경우, 인스턴스가 유지되며 state도 보존됨. 대신 하위 컴포넌트에는 변경 사항을 componentWillReceiveProps()componentWillUpdate()로 전파함.

    자식노드가 갱신될 때는 신경써야 한다.

    // 1.
    <ul>
      <li>Edward</li>
    </ul>
    
    // 2. 뒤로 추가되는 경우에는 기존 엘리먼트가 유지됨
    <ul>
      <li>Edward</li>
      <li>Mindy</li>
    </ul>
    
    // 3. 앞으로 추가되는 경우에는 노드 전체를 다시 그림
    //    당연히 성능 하락 발생하며 컴포넌트 엘리먼트 경우
    //    언마운트 마운트하게 된다
    <ul>
      <li>Mindy</li>
      <li>Edward</li>
    </ul>
    
    // 4. 앞서의 가정 2에 따라서 `key`를 제공하면
    //    새로 그리지 않고 반영할 수 있게 됨
    <ul>
      <li key="1029">Edward</li>
    </ul>
    
    <ul>
      <li key="2012">Mindy</li>
      <li key="1029">Edward</li>
    </ul>
    

    id가 없다면 적당히 hash를 생성해서 쓴다. 동일 계층에서만 유일값을 가지면 된다. 최후의 수단은 배열의 index인데 배열 순서가 바뀌지 않는다는 가정이 있어야 한다. 순서가 바뀌면 key를 써도 느리다. 별로 권장하지 않는다.

    최종적인 결과는 동일하지만 어떻게 구현되어 있는지 아는 것으로 성능 향상을 할 수 있다. 휴리스틱에 기반한 알고리즘이라서 다음 경우엔 좋지 않다.

    1. 하위 트리의 컴포넌트가 일치하는지 검사하지 않는다. 두 컴포넌트가 비슷한 결과를 낸다면 하나로 만드는걸 고려한다.
    2. 키는 안정적이고 예측 가능하며 유일해야 한다. 이 규칙을 지키지 않으면 성능 하락이나 자식 컴포넌트가 state를 잃어버리는 경우가 발생한다.

    Context

    props로 일일이 내려주기 번거로울 때 contextTypes로 지정하면 하위 트리에 전역으로 전달된다. prop-types가 필요하다. 흑마법이므로 사용하지 말 것. 실험적인 API로 차후 문제가 될 가능성이 높다. 문서 내내 쓰지 말라는 말이 반복된다.

    Fragments

    엘리먼트를 반환할 때 컨테이너 역할을 하는 <div> 등을 쓰기 마련인데 테이블 같은 걸 조립할 때는 마크업에 맞지 않다. 이런 경우를 위해 정말 컨테이너 역할만 하는 <React.Fragment>를 사용할 수 있다.

    class Columns extends React.Component {
      render() {
        return (
          <React.Fragment>
            <td>Hello</td>
            <td>World</td>
          </React.Fragment>
        );
      }
    }
    

    약식 표기로 <>, </>를 사용할 수 있지만 현재 툴에서 지원하지 않을 수도 있으니 위 방식으로 사용한다.

    <React.Fragment>로 묶을 때 key도 지정할 수 있다.

    function TodoList(props) {
      return (
        <dl>
        {props.items.map(item => (
          <React.Fragment key={item.id}>
            <dt>{item.thing}</dt>
            <dd>{item.done}</dd>
          </React.Fragment>
        ))}
        </dl>
      );
    }
    

    Portals

    컴포넌트를 부모 컴포넌트의 DOM 트리 바깥에 붙일 때 사용하는 방법이다. 전체화면에 모달 띄우기 이런 동작 필요할 때 쓴다. 아래 코드에서 child는 렌더링 가능한 리액트 엘리먼트고 container는 DOM 엘리먼트다.

    ReactDOM.createPortal(child, container)
    

    DOM에서 이벤트 전파(Event bubbling)는 실제 노드를 타고 올라가는 식이지만 포털은 다른 DOM 노드에 위치하고 있더라도 React 컴포넌트의 노드 트리를 타고 전파된다. DOM의 바인딩만 트리 바깥에서 일어나고 실제 모든 컴포넌트는 기존과 동일한 방식으로 동작한다.

    에러 바운더리

    기존엔 에러가 발생하면 컴포넌트 트리가 멍텅구리 되었는데 에러 바운더리를 사용해서 해결할 수 있다. 오류가 발생했을 때 동작을 componentDidCatch(error, info)에 선언한다. error는 발생한 오류고 infocomponentStack 키를 포함한 객체로 오류 발생 시, 스택 정보를 제공한다.

    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props);
        this.state = { hasError: false };
      }
    
      componentDidCatch(error, info) {
        // 오류 발생 UI 표시
        this.setState({ hasError: true });
        // 로그로 남김
        logErrorToMyService(error, info);
      }
    
      render() {
        if (this.state.hasError) {
          return <h1>Something went wrong.</h1>;
        }
        return this.props.children;
      }
    }
    
    <ErrorBoundary>
      <MyWidget />
    </ErrorBoundary>
    

    에러 바운더리는 다음처럼 컨텍스트 바깥에서 발생하는 오류는 잡지 못한다.

    • 이벤트 핸들러
    • 비동기 코드
    • 서버측 렌더링
    • 하위 컴포넌트가 아닌 에러 바운더리 자체에서 발생한 오류

    react 16부터는 에러 바운더리로 에러가 잡히지 않았을 때 리액트 컴포넌트 트리 전체를 언마운트한다. 문제 있는 UI를 그대로 두면 사용자의 잘못된 조작을 야기할 수 있기 때문이라고 한다.

    create react app 사용하면 스택 추적에서 어디서 오류가 났는지 명확히 보인다. create react app을 사용하지 않는다면 플러그인을 설치하면 된다. 다만 프로덕션에서는 꺼야 한다.

    try ... catch는 명령행 코드에서만 동작한다. 이벤트 핸들러의 에러는 이벤트 바운더리에서 잡지 못한다. 이벤트 핸들러의 에러는 try ... catch를 사용한다.

    15에서는 unstable_handleError였다고 한다.

    웹 컴포넌트

    웹 컴포넌트를 react 내에서 사용할 경우에는 일반 DOM 컴포넌트를 사용하는 것처럼 쓸 수 있다. 웹 컴포넌트는 명령형 API를 쓰는 경우가 종종 있는데 ref로 참조를 받아와 DOM 노드를 직접 호출해야 할 수도 있다. 또한 웹 컴포넌트에서 발생한 이벤트가 리액트 컴포넌트에 제대로 전이되지 않을 수 있으므로 직접 핸들러를 연결해야 할 수도 있다.

    웹 컴포넌트에서 React 컴포넌트를 사용하려면 ReactDOM.render직접 렌더링 해야한다.

    고차 컴포넌트

    고차 컴포넌트는 컴포넌트를 입력 받아 새로운 컴포넌트를 반환하는 함수를 의미한다. (고차 함수와 같은 접근 방식이다.) Redux의 connect, Relay의 createFragmentContainer도 동일한 방식이다.

    function withSubscription(WrappedComponent, selectedData) {
      return class extends React.Component {
        // ...
      }
    }
    

    고차 컴포넌트 내에서 기존 컴포넌트를 변경하지 않도록 주의한다. 고차 컴포넌트 내에서 기존 컴포넌트를 변경하면 추상성이 무너진다. 순수 함수처럼 작성해야 한다. 이 접근 방식은 책임을 분리한다는 점에서 컨테이너 컴포넌트와 유사한 점이 있다.

    render() {
      // 불필요한 prop을 제거하거나 추가적으로 필요한 prop을 전달한다.
      const { extraProp, ...passThroughProps } = this.props;
      const injectedProp = someStateOrinstanceMethod;
      return (
        <WrappedComponent
          injectedProp={injectedProp}
          {...passThroughProps}
        />
      );
    }
    

    compose 같은 합성 함수를 사용하면 편리하다. lodashRedux, Ramda에서도 제공한다.

    고차 컴포넌트에서 반환하기 전에 displayName을 추가하면 디버그를 쉽게 할 수 있다.

    function withSubscription(WrappedComponent) {
      class WithSubscription extends React.Component { /* ... */ };
      WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
      return WithSubscription;
    }
    
    function getDisplayName(WrappedComponent) {
      return WrappedComponent.displayName || WrappedComponent.name || 'Component';
    }
    

    고차 컴포넌트에서 지켜야 할 사항이 있다.

    1. render() 내에서는 고차 컴포넌트를 사용하지 않는다. 렌더링 할 때마다 새로운 컴포넌트를 만들고 최악의 경우 노드의 모든 상태를 잃게 된다.
    2. 고차 컴포넌트를 만들 때는 정적 메소드도 직접 복사해야 한다. hoist-non-react-statics 같은 패키지를 사용해도 된다.
    3. 고차 컴포넌트는 ref를 전달하지 못한다. ref는 일반 prop이 아니기 때문인데 부모 컴포넌트에 ref 노출하는 식으로 별도의 prop을 만들어서 전달해야 한다. 깔끔한 해결책은 아니다.

    Render Props

    Render props은 prop에 엘리먼트를 반환하는 함수를 전달해서 재사용성을 높이는 방법이다. React Routerdownshift에서 사용하는 방식이라고 한다.

    <Mouse render={mouse => (
      <Cat mouse={mouse} />
    )}/>
    

    구체적으로 구현하는 방식보다 동작을 사용자에게 위임하는 방식으로 구현하는 접근법으로 Mouse 커서 위치를 전달하는 예를 들었다.

    이런 방식으로 사용하는걸 render props라고 하지만 꼭 props가 render일 필요는 없다. 패턴 이름일 뿐이다.

    설명에는 ShallowEquals 때문에 React.PureComponent에서는 익명함수가 계속 새로 생성된다고 나오는데 내가 테스트를 제대로 못하는건지 React.Component에서 하는거랑 차이가 없어 보인다. 여튼 익명함수를 반복해서 생성하고 싶지 않다면 익명함수 대신 선언한 메소드를 전달해주는 방식으로 해결할 수 있다.

    constructor(props) {
      super(props);
      this.renderTheCat = this.renderTheCat.bind(this);
    }
    // ...
    renderTheCat(mouse) {
      return <Cat mouse={mouse} />;
    }
    // ...
    return <Mouse render={this.renderTheCat} />;
    

    다른 라이브러리와 함께 사용하기

    DOM을 직접 제어하는 플러그인과 함께 사용하려면 ref로 DOM 엘리먼트를 노출하고 직접 제어한다.

    componentDidMount() {
      this.$el = $(this.el);
      this.$el.chosen();
    }
    
    componentWillUnmount() {
      this.$el.chosen('destroy');
    }
    render() {
      return (<select ref={el => this.el = el}>{this.props.children}</select>);
    }
    

    다른 뷰 라이브러리와 연동하기에서는 리액트로 포팅하는 방법이랑 Backbone.View에 리액트 컴포넌트를 어떻게 넣는지 설명한다.

    React state, flux, redux를 권하긴 하지만 모델 레이어와도 통합이 가능하다.

    Backbone의 모델을 컴포넌트에서 사용하려면 backbone에서 사용하는 방식대로 사용하면 되고 Backbone의 모델에서 데이터를 가져오는 방식으로는 고차 컴포넌트 형태로 활용할 수 있다.

    설명은 Backbone으로 했지만 여기서 사용한 기법 자체는 제한적이지 않다.

    접근성

    WAI-ARIA 적용, 시멘틱 HTML, 폼 접근성, 포커스 컨트롤 등 접근성 관련된 내용을 설명하는데 읽어봐야 하는 문서를 전부 나열하고 있다. 리액트에 특정한 부분이 아니라서 각 링크는 본문을 확인한다.

    // for 대신 htmlFor를 사용
    <label htmlFor="namedInput">Name:</label>
    <input id="namedInput" type="text" name="name"/>
    

    포커스 제어는 ref로 직접 DOM을 받아서 처리한다.

    코드 분할

    import() 등 nodejs에서 사용하는 일반적인 코드 분할 기법을 설명한다.

    색상을 바꿔요

    눈에 편한 색상을 골라보세요 :)

    Darkreader 플러그인으로 선택한 색상이 제대로 표시되지 않을 수 있습니다.