미남이의 이러쿵저러쿵 https://appletree.or.kr/blog Open Web 짝사랑.  iOS Programming. Wed, 07 Feb 2018 00:52:42 +0000 en-US hourly 1 https://wordpress.org/?v=5.4.2 Post-Processor를 이용한 깔끔하고 미래 지향적인 CSS 작성 https://appletree.or.kr/blog/web-development/css/post-processor%eb%a5%bc-%ec%9d%b4%ec%9a%a9%ed%95%9c-%ea%b9%94%eb%81%94%ed%95%98%ea%b3%a0-%eb%af%b8%eb%9e%98-%ec%a7%80%ed%96%a5%ec%a0%81%ec%9d%b8-css-%ec%9e%91%ec%84%b1/ https://appletree.or.kr/blog/web-development/css/post-processor%eb%a5%bc-%ec%9d%b4%ec%9a%a9%ed%95%9c-%ea%b9%94%eb%81%94%ed%95%98%ea%b3%a0-%eb%af%b8%eb%9e%98-%ec%a7%80%ed%96%a5%ec%a0%81%ec%9d%b8-css-%ec%9e%91%ec%84%b1/#comments Sun, 31 May 2015 05:02:14 +0000 http://appletree.or.kr/blog/?p=2746 웹을 떠받치는 기술은 모두 비슷한 사정이지만, 특히나 CSS 작성 결과물은 개개인의 코딩 습관과 숙련도 그리고 유동적인 브라우저 지원 상황에다 반갑지 않은 버그 등 여러 외적 요인이 모여서 큰 변수로 작용하기 마련이다. 표면적으론 단순해 보이지만, 자유의지에 맡겨진 properties 지정 작업은 픽셀 하나하나 차곡차곡 쌓아 올린 작용과 반작용의 만남이다. 이것은 잔잔한 브라우저 창에 파도의 물결을 일으키는 상상력의 실현이 될 수도, 혹은 의도치 않은 부작용 때문에 엉키고 뒤틀려버린 혼란의 골칫거리가 되버릴 수도 있다. 그래서 항상 집중과 주의가 요구되는 고난의도 작업.

이런 복잡 미묘한 상황에 조금이라도 외적 잡음을 줄이고 동시에 CSS 작성의 효율을 높이고자 OOCSS, SMACSS, BEM과 같은 여러 구조적 방법론들이 계속 등장하고 있으며, 또한 공동 작업의 편의를 위한 CSS 코딩 스타일 통합을 목적으로 properties 지정 순서에서부터 공백의 개수, 따옴표 스타일 등 아주 세세하고 민감한 부분까지 일관되게 정리해주는 CSS 빗질 도구까지 마련되어 있다.

여기서 한발 더 나아가 SASS, Less와 같은 일명 Pre-Processor의 힘을 빌려 CSS 작성 효율을 구조적으로 좀 더 끌어올리려는 노력도 활발한데, 최근엔 한술 더 떠서 CSS 후처리 과정까지 더해준 Post-Processor들도 덩달아 비 온 뒤 잡풀처럼 여기저기 생겨나고 있어서 이들의 태생 목적과 사용법을 소개하고자 한다.

CSS Pre-Processors vs. Post-Processors

CSS Pre-Processor는 기존 CSS 문법의 확장 성격으로 자기만의 syntax로 작성하고 parse/compile 과정을 거쳐 일반 CSS로 되돌려준다. 이에 반해 Post-Processor는 그냥 맨살의 CSS 문법으로 작성된 것을 해석/처리해서 다시 일반 CSS를 돌려주는 차이가 있다.

정의만 살펴보면, Post-Processor를 써서 얻을 수 있는 장점을 이해하기 힘든데, Post-Processor가 제공하고 있는 기능 중 가장 많이 애용되는 Autoprefixer의 사용 예를 보면 Post-Processor만의 장점이 뚜렷해진다.

CSS3 규칙 중 브라우저 지원 때문에 prefix를 일일이 붙여줘야 하는 상황이라면, Sass(SCSS)의 경우 mixin 기능으로 다음과 같이 적용해 줄 수 있다.

@mixin flexbox() {
  display: -webkit-box;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
}
 
.box {
  @include flexbox();
}

하지만 Autoprefixer의 힘을 빌리면 CSS 작성은 아주 단순해진다.

.box { display: flex; }

원래의 CSS 문법을 그대로 작성하면, 후처리 과정을 거쳐 골치 아픈 vender prefix를 더해 다음과 같이 돌려준다.

.box {
  display: -webkit-box;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
}

특정 CSS property의 브라우저 지원 상황을 일일이 확인해 가면서 mixin을 써야 할지 말아야 할지 고민할 필요가 없는 것이다.
Post-Processor(PostCSS) 소개 슬라이드: PostCSS: the Future after Sass and Less

Post-Processor가 제공하는 후처리 기능

앞서 설명한 Autoprefixer와 같은 Post-Processor는 대표적으로 PostCSS 혹은 rework의 plugin 형태로 많이 개발되고 있는데, 이런 plugin들을 모아 놓은 일종의 plugins pack으로서 지금은 cssnext, Pleease, Stylecow가 대표적으로 눈에 띈다.

여기선 PostCSS 기반 cssnext가 제공하는 기능을 살펴보고자 한다. (어차피 어느 Post-Processor를 고르더라도, 지원하는 기능의 차이만 있을 뿐, CSS 표준 문법으로 작성한 코드를 해석해서 돌려주는 산출 코드는 거의 같으므로 실제 CSS 코드 작성 방식에는 영향을 주지 않는다. 이것도 Post-Processor만의 사용 장점 중 하나.)

Use tomorrow’s CSS syntax, today.

– cssnext

cssnext가 내건 슬로건으로, 브라우저 지원 따윈 신경 쓰지 말고 차기 CSS 스펙을 그대로 가져와 지금 당장 써보자는 문구를 표방하고 있다. JavaScript의 Babel compiler와 비슷한 격으로 CSS3/4 spec을 위한 polyfill들을 모아놓은 셈이다.

먼저, 설치는 npm module로 터미널에서 다음 한 줄을 입력한다.

$ npm install -g cssnext

CSS 작성 후 기본 compile은 다음과 같다.

$ cssnext input.css output.css

편의를 위해 CSS 파일 내용이 바뀔 때마다 자동 compile 되는 watch 기능도 제동한다.

$ cssnext --watch input.css output.css

더 자세한 설치와 사용 방법은 cssnext 설치/설정 페이지에서 확인할 수 있다.

앞서 설명한 vendor prefixes 자동 추가 기능 이외 나머지 기능도 살펴보자.

Custom Propertie & Var()

W3C의 CSS Custom Properties for Cascading Variables Module을 지원하는데, 후처리 과정의 한계로 :root selector에 지정된 custom properties만 사용할 수 있다.

사용 예, input.css :

:root {
  --color: red;
}
 
div {
  color: var(--color);
}

output.css :

div {
  color: red;
}

CSS Variables (Custom Properties)

Permits the declaration and usage of cascading variables in stylesheets.

W3C Candidate Recommendation

Supported from the following versions:

Desktop

  • 49
  • 31
  • No
  • 36
  • 9.1

Mobile / Tablet

  • 9.3
  • 122
  • 80
  • 122
  • 123

* denotes prefix required.

  • Supported:
  • Yes
  • No
  • Partially
  • Polyfill

Stats from caniuse.com

Reduced calc()

CSS3 calc() function의 expression 안에서 CSS variables를 사용할 수 있게 해준다. 다만, 서로 다른 수치의 단위가 expression에서 사용되었다면 브라우저가 알아서 해석하도록 그대로 놔둠.

input.css :

:root {
  --main-font-size: 16px;
}
 
body {
  font-size: var(--main-font-size);
}
 
h1 {
  font-size: calc(var(--main-font-size) * 2);
  height: calc(100px - 2em);
}

output.css :

body {
  font-size: 16px;
}
 
h1 {
  font-size: 32px;
  height: calc(100px - 2em);
}

calc() as CSS unit value

Method of allowing calculated values for length units, i.e. `width: calc(100% - 3em)`

W3C Candidate Recommendation

Supported from the following versions:

Desktop

  • 19*
  • 4*
  • 9
  • 15
  • 6*

Mobile / Tablet

  • 6.0*
  • 4.4
  • 80
  • 122
  • 123

* denotes prefix required.

  • Supported:
  • Yes
  • No
  • Partially
  • Polyfill

Stats from caniuse.com

Custom Media Queries

종종 같은 media query를 여러 번 반복 지정해서 사용할 경우 나중에 수정할 일이 생기면 관리적 측면에서 성가신 작업이 될 수 있다. 그래서 W3C CSS Custom Media Queries가 제정되었는데, 이것은 일종의 길고 복잡한 media query 대신 짧고 알아보기 쉬운 이름을 한 곳에 지정해 놓고 쉽게 여러 곳에서 사용할 수 있도록 해준다.

input.css :

@custom-media --small-viewport (max-width: 30em);
 
@media (--small-viewport) {
  /* styles for small viewport */
}

output.css :

@media (max-width: 30em) {
  /* styles for small viewport */
}

CSS3 Media Queries

Method of applying styles based on media information. Includes things like page and device dimensions

W3C Recommendation

Supported from the following versions:

Desktop

  • 4
  • 3.5
  • 5.5
  • 9.5
  • 3.1

Mobile / Tablet

  • 3.2
  • 2.1
  • 10
  • 122
  • 123

* denotes prefix required.

  • Supported:
  • Yes
  • No
  • Partially
  • Polyfill

Stats from caniuse.com

Media Queries Ranges

Media Queries Ranges는 media query 괄호 안 media feature type 중 어떤 범위나 수치를 지정 테스트할 때 쓰는 min-/max- 접두사 대신 읽고 쓰기 편한 <= 그리고 >= 기호를 써서 표현할 수 있게 해준다.

input.css :

@media screen and (width >= 500px) and (width <= 1200px) {
  .bar {
    display: block;
  }
}
 
@media screen and (500px <= width <= 1200px) {
  .foo {
    display: block;
  }
}

output.css :

@media screen and (min-width: 500px) and (max-width: 1200px) {
  .bar {
    display: block;
  }
}
 
@media screen and (min-width: 500px) and (max-width: 1200px) {
  .foo {
    display: block;
  }
}

Custom Selectors

Custom Selectors는 Selector들의 조합을 가지고 이를 대표하는 자기만의 이름을 지정해서 사용할 수 있게 해준다. 일일이 selector를 치는 타이핑의 수고를 절약할 수 있는 방법이다.

input.css :

@custom-selector :--heading h1h2h3h4h5h6;
 
@custom-selector :--any-link :link:visited;
 
article :--heading + p {
  margin-top: 0;
}
 
a:--any-link {
  color: blue;
}

output.css :

article h1 + p,
article h2 + p,
article h3 + p,
article h4 + p,
article h5 + p,
article h6 + p {
  margin-top: 0;
}
 
a:link,
a:visited {
  color: blue;
}

color()

color() function은 색상 지정을 좀 더 미세하게 조정할 필요가 있을 때 사용하는데, 기본 바탕 색에다 여러 color adjusters를 써서 표현하고 싶은 색을 조작할 수 있다. 산출물로 호환성을 지닌 rgba() 값을 돌려준다.

input.css :

body {
  color: color(red a(10%));
  background-color: color(red lightness(50%));
  border-color: color(hsla(12550%50%.4) saturation(+10%) w(-20%));
}

output.css :

body {
  color: #FF0000;
  color: rgba(255, 0, 0, 0.1);
  background-color: rgb(255, 0, 0);
  border-color: #00CC11;
  border-color: rgba(0, 204, 17, 0.4);
}

hwb()

CSS hwb() function은 hue color에다 white와 black의 조합을 가미해서 기존 hsl()보다 색상 조절 작업이 더 수월한 장점이 있다. 산출물로 호환성을 지닌 rgba() 값을 돌려준다.

input.css :

body {
  color: hwb(900%0%0.5);
}

output.css :

body {
  color: rgba(128, 255, 0, 0.5);
}

gray()

gray() function으로 색 빠진 회색 조절을 좀 더 섬세하게.

input.css :

.foo {
  color: gray(0);
}
.bar {
  color: gray(25550%);
}

output.css :

.foo {
  color: rgb(0, 0, 0);
}
.bar {
  color: rgba(255, 255, 255, 0.5);
}

rrggbbaa

alpha 값이 추가된 W3C RGBA hexadecimal notations으로 #RRGGBBAA 혹은 #RGBA 형식을 갖는다.

input.css :

body {
  background: #9d9c;
}

output.css :

body {
  background: rgba(153, 221, 153, 0.8);
}

rebeccapurple

CSS language에 많은 공헌을 한 Eric Meyer씨의 딸이 너무 일찍 세상을 떠나면서, 추념의 의미로 그녀가 좋아했던 자줏빛 색(#663399)에 이름을 넣어 CSS Color Level 4에 추가한 사연이 들어 있다.

color: rebeccapurplecolor: rgb(102, 51, 153)로 변환된다.

font-variant

W3C CSS font variant properties에 브라우저 지원 상황이 좀 더 양호한 font-feature-settings fallback을 추가.

input.css :

h2 {
  font-variant-caps: small-caps;
}
 
table {
  font-variant-numeric: lining-nums;
}

output.css :

h2 {
  font-feature-settings: "c2sc";
  font-variant-caps: small-caps;
}
 
table {
  font-feature-settings: "lnum";
  font-variant-numeric: lining-nums;
}

CSS font-feature-settings

Method of applying advanced typographic and language-specific font features to supported OpenType fonts.

W3C Recommendation

Supported from the following versions:

Desktop

  • 16*
  • 4*
  • 10
  • 15*
  • 4

Mobile / Tablet

  • 3.2
  • 4.4*
  • 80
  • 122
  • 123

* denotes prefix required.

  • Supported:
  • Yes
  • No
  • Partially
  • Polyfill

Stats from caniuse.com

filter

CSS filter property에 지정한 filter fuction에 대응하는 inline <svg> filter fallback을 추가.

input.css :

.blur {
  filter: blur(4px);
}

output.css :

.blur {
  filter: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg"><filter id="filter"><feGaussianBlur stdDeviation="4" /></filter></svg>#filter');
  filter: blur(4px);
}

CSS Filter Effects

Method of applying filter effects using the `filter` property to elements, matching filters available in SVG. Filter functions include blur, brightness, contrast, drop-shadow, grayscale, hue-rotate, invert, opacity, sepia and saturate.

W3C Working Draft

Supported from the following versions:

Desktop

  • 18*
  • 3.6
  • No
  • 15*
  • 6*

Mobile / Tablet

  • 6.0*
  • 4.4*
  • 80
  • 122
  • 123

* denotes prefix required.

  • Supported:
  • Yes
  • No
  • Partially
  • Polyfill

Stats from caniuse.com

rem units

rem units을 지원하지 않는 브라우저를 위해 px fallback 추가. (root font-size의 기본값은 16px이며, html 혹은 :root에 지정된 font-size에 따라 그 기준 기본값이 바뀐다.)

input.css :

h1 {
  font-size: 2rem;
}

output.css :

h1 {
  font-size: 32px;
  font-size: 2rem;
}

rem (root em) units

Type of unit similar to `em`, but relative only to the root element, not any parent element. Thus compounding does not occur as it does with `em` units.

W3C Candidate Recommendation

Supported from the following versions:

Desktop

  • 4
  • 3.6
  • 9
  • 11.
  • 5

Mobile / Tablet

  • 4.0
  • 2.1
  • 12
  • 122
  • 123

* denotes prefix required.

  • Supported:
  • Yes
  • No
  • Partially
  • Polyfill

Stats from caniuse.com

pseudo-elements

pseudo-elements의 두 개짜리 콜론(::)을 한 개짜리(:)로 치환. (pseudo-classes를 위한 한 개짜리 콜론 용법과 구별하려고 콜론 두 개가 사용되는데, IE9 미만 버전에선 두 개짜리 CSS3 용법을 지원하지 않는다.)

input.css :

p::first-line {
  text-transform: uppercase;
}

output.css :

p:first-line {
  text-transform: uppercase;
}

:any-link pseudo-class

:any-link pseudo class 지원. 지금 쓰이는 :link는 이름에서 약간의 혼동 소지가 있는 게, 모든 하이퍼링크를 대표하는 게 아니라 그 중 방문 기록이 없는(unvisited) 링크만을 상징한다. 그래서, :link:visited를 모두 아우르는 :any-link가 제안되었다.

input.css :

nav :any-link {
  background-color: yellow;
}

output.css :

nav :link,
nav :visited {
  background-color: yellow;
}

:matches pseudo-class

:matches pseudo-class는 selector 리스트를 argument로 받아 해당의 각 selector를 모두 아우르는 대표로서 여러 selector를 한꺼번에 간편하게 지정할 때 사용되는데, 글보단 사용 예를 보면 이해가 쉽다.

input.css :

p:matches(:first-child.special) {
  color: red;
}

output.css :

p:first-childp.special {
  color: red;
}

:not pseudo-class

:not pseudo-class는 앞서 설명한 :matches pseudo-class의 반대 성격을 갖고 있으나, 사용법은 비슷하다.

input.css :

p:not(:first-child.special) {
  color: blue;
}

output.css :

p:not(:first-child)p:not(.special) {
  color: blue;
}

rgba() to hexadecimal color

CSS 3 Colors에 정의된 rgba color 값을 인식하지 못하는 브라우저를 위해 hexadecimal color fallback 추가.

input.css :

body {
  background: rgba(153, 221, 153, 0.8);
}

output.css :

body {
  background: #99DD99;
  background: rgba(153, 221, 153, 0.8);
}

CSS3 Colors

Method of describing colors using Hue, Saturation and Lightness (hsl()) rather than just RGB, as well as allowing alpha-transparency with rgba() and hsla().

W3C Recommendation

Supported from the following versions:

Desktop

  • 4
  • 2
  • 9
  • 9.5
  • 3.1

Mobile / Tablet

  • 3.2
  • 2.1
  • 10
  • 122
  • 123

* denotes prefix required.

  • Supported:
  • Yes
  • No
  • Partially
  • Polyfill

Stats from caniuse.com

@import

관리를 쉽게 하려고 따로 빼어 놔두었던 @import로 지정된 스타일을 통째로 불러들여 통합해 주는 기능.

input.css :

@import "layout.css";
 
@import "desktop.css" (min-width: 30em);
 
body {
  background: black;
}

output.css :

/* layout.css 내용 */
 
@media (min-width: 30em) {
  /* desktop.css 내용 */
}
 
body {
  background: black;
}

마무리

지금까지 소개한 차세대 CSS 기술들을, 이중 혹 일부만 쓰더라도, 브라우저 지원의 고민 없이 마음껏 써볼 수 있다는 것은 참 매력적이다. 결국, 이렇게 해서 얻어진 작업의 효율은 힘을 보태는 추진력이 되어 더 멋지고 단단한 결과물을 끌어낼 수 있기 때문이다.

전부터 이미 많은 개발자의 CSS 작성 관리 도구로 자리를 잡은 CSS Pre-Processor 그리고 여기서 살펴본 Post-Processor의 기능은 서로 겹치는 부분이 있지만, 활용에 따라 자기 영역에서 더 빛을 발하는 구석이 분명해서 서로 보완적인 관계라고 생각한다. 그래서 둘 만의 장점을 잘 골라서 활용한다면, 효율적이고 깔끔한 CSS 코드를 작성하는 데 좋은 시너지 효과를 불러올 수 있겠다.

]]>
https://appletree.or.kr/blog/web-development/css/post-processor%eb%a5%bc-%ec%9d%b4%ec%9a%a9%ed%95%9c-%ea%b9%94%eb%81%94%ed%95%98%ea%b3%a0-%eb%af%b8%eb%9e%98-%ec%a7%80%ed%96%a5%ec%a0%81%ec%9d%b8-css-%ec%9e%91%ec%84%b1/feed/ 2
Modules, 앞으로 JavaScript 라이브러리가 나아갈 길 https://appletree.or.kr/blog/web-development/javascript/modules-%ec%95%9e%ec%9c%bc%eb%a1%9c-javascript-%eb%9d%bc%ec%9d%b4%eb%b8%8c%eb%9f%ac%eb%a6%ac%ea%b0%80-%eb%82%98%ec%95%84%ea%b0%88-%ea%b8%b8/ https://appletree.or.kr/blog/web-development/javascript/modules-%ec%95%9e%ec%9c%bc%eb%a1%9c-javascript-%eb%9d%bc%ec%9d%b4%eb%b8%8c%eb%9f%ac%eb%a6%ac%ea%b0%80-%eb%82%98%ec%95%84%ea%b0%88-%ea%b8%b8/#respond Sat, 02 Aug 2014 05:38:16 +0000 http://appletree.or.kr/blog/?p=2629 이 글은 Modules, a Future Approach to JavaScript Libraries – Tuts+ Code Article의 글을 한국어로 옮긴 것입니다. (참고로, 지금까지 이곳에 올린 글 중에 가장 긴 게 아닌가 생각되네요.)

jQuery와 같은 JavaScript 라이브러리는 거의 지난 10여 년 동안 웹 브라우저에서 실행될 JavaScript를 작정할 때마다 당연히 가장 먼저 추가해야만 되는 것으로 여겨지고 있습니다. 이러한 현상은 각종 브라우저 간 차이와 구현 문제로 어쩔 수 없었으며, 그래서 지금까지 그 임무를 훌륭히 수행하고 있습니다. jQuery는 이러한 브라우저 버그와 각자 기괴하게 꼬인 것들을 알아서 말끔하게 덮어주면서 가령 이벤트 처리나 Ajax 그리고 DOM 조작과 같은 작업을 손쉽게 만들어 주었습니다.

이땐, jQuery가 거의 모든 문제를 해결해 주었기에, 자연스레 추가해서 얻은 막강한 힘을 빌려 그냥 작업에만 바로 집중할 수 있었지요. 이것은 마치 브라우저가 제대로 작동하려면 있어야 하는 블랙박스와 같았습니다.

하지만 지금, 웹은 진보하였고, API도 많이 개선되었으며 마찬가지로 웹 표준도 그만큼 더 많이 구현되기 시작하면서 아주 빠르게 변화하는 환경이 되었습니다. 그래서 과연 미래의 브라우저에도 이런 거대한 라이브러리들이 필요로 할지 의문을 가질 수밖에 없게 되었습니다. 결국 모듈(Module)에 적합한 환경으로 변화하고 있는 것입니다.

모듈 소개

모듈은 어떤 하나의 특화된 기능을 아주 잘 수행하는 어떤 독립된 집합체라 할 수 있습니다. 예를 들어, 모듈은 어떤 특정 엘르먼트에 클래스를 추가하는 기능을 수행하거나, 혹은 Ajax를 이용한 HTTP 통신을 하는 등 그 가능성은 무한합니다.

모듈은 그 모양과 크기가 여러 가지 있는데, 일반적인 목적은 하나같이 작업 환경에 불러오기만 하면 알아서 원래 가지고 있는 기능을 잘 수행하는 데 있습니다. 보통 모듈은 기본적으로 각각의 개발자 문서와 설치 과정 그리고 브라우저나 서버와 같은 실행 환경의 정보를 제공하고 있습니다.

이러한 모듈은 프로젝트에 필요한 기능을 메꿔주면서 나중에 추가와 제거와 같은 의존성 관리가 훨씬 간편해지는 효과가 있습니다. 모듈이 제공하는 장점과 유연성을 생각해보면, 이젠 어떤 덩치 큰 라이브러리를 단순하게 추가하는 일은 점점 줄어들 것입니다. 다행스럽게도 jQuery와 같은 라이브러리들도 이점을 인식해서 필요한 기능만 내려받을 수 있는 온라인 도구를 제공하기 시작했습니다.

최근에 소개된 API들은 모듈로 작성될 수도 있는 영감을 북돋게 하는 커다란 촉진제 역할을 하였고, 이젠 브라우저의 구현 상황도 상당히 좋아져서, 대부분의 일상적 작업은 작은 기능의 모듈을 작성해서 대신 처리해 줄 수도 있게 되었습니다.

모듈의 시대는 이미 도래했으며, 앞으로도 오랫동안 그 소임을 계속 맡아주리라 기대되고 있습니다.

첫 번째 모듈의 영감

최근 공개된 API 중 처음 소개되고 나서 모듈로 삼을 만한 것으로 항상 관심이 있었던 것이 바로 classList API입니다. jQuery와 같은 라이브러리에서 영감을 얻어서, 이젠 다른 라이브러리 없이도 기본적으로 브라우저에서 엘르먼트에 CSS 클래스를 더하는 길이 생긴 겁니다.

classList API가 소개된 지는 벌써 몇 년이 지났지만, 많은 개발자가 그 존재를 아직 모르고 있습니다. ​그래서 이 classList API을 활용해서, 이를 지원하지 않는 브라우저에선 이 기능을 대신하는 모듈을 한 번 만들어보기로 하였습니다.

코드를 바로 설명하기 전에, 우선 jQuery가 엘르먼트에 클래스를 어떻게 더해주는지 살펴보겠습니다.:

$('elem').addClass('myclass');

이런 조작 기법이 브라우저가 지원하는 기본 기능에 추가된 것이 바로 앞서 언급된 classList API인데, classList API는 엘르먼트의 className에 저장된 값에 해당하는 공백으로 분류된 DOMTokenList Object를 조작하는 몇 가지 방법을 제공하고 있으며, 이는 jQuery의 처리 방식과 많이 닮았습니다. 아래는 classList API를 이용해서 클래스를 추가해주는 예제입니다.:

$('elem').addClass('myclass');

여기서 우리가 얻을 수 있는 교훈은 무엇일까요? 특정 라이브러리의 기능이 기본 플랫폼 언어로 자리 잡는 것은 대단한 일이고, 그렇지 않더라도 어떤 영감을 줄 수도 있을 것입니다. 이것이야말로 열린 웹 플랫폼의 장점이고, 앞으로 나아가는 데 누구나 조력자가 될 수도 있다는 사실입니다.

그럼 본론으로 들어가 볼까요? 이제 모듈에 대한 이해를 마쳤고, 또 classList API도 마음에 들지만, 안타깝게도 아직 모든 브라우저가 이것을 지원하지는 않습니다. 그렇지만 적어도 비슷한 대체 기능을 작성할 수는 있겠지요. 브라우저가 지원하면 classList를 쓰고, 그렇지 않다면 자동으로 대체 기능을 구현해주는 모듈을 작성하는 건 좋은 아이디어라고 생각됩니다.

첫번째 모듈 작성: Apollo.js

대략 약 여섯 달 전에, 일반 JavaScript로 엘르먼트에 클래스를 더해주는 아주 간단하고 독립적인 모듈을 작성했었는데, 나중엔 이름을 apollo.js라고 붙여 주었습니다.

모듈을 작성하기로 마음먹은 이유는 훌륭한 classList API를 쓰기 시작하면서, 이런 간단하고 일반적인 작업에 어떤 라이브러리를 끌어들이진 말자고 하는 것이 주된 이유였습니다. jQuery는 전에도 그랬고 지금도 아직 classList API를 사용하고 있지 않아서, 새로운 기술을 실험해 볼 수 있는 아주 좋을 기회라고 생각했습니다.

어떻게 만들었는지의 설명과 함께 결과적으로 간단한 모듈이 탄생하기까지의 각 과정을 공개합니다.

classList 사용

이미 보셨듯이, classList는 매우 멋진 API라서 “jQuery에 익숙한 개발자”라도 적응하기가 아주 쉽습니다. 다만, 한 가지 마음에 안 드는 것은 API가 제공하는 매소드(method)를 쓸려면 항상 classList Object를 우선 지칭해서 써야 한다는 점입니다. 그래서 저는 apollo를 작성할 때 이런 불필요하게 반복된 작업을 없애려 했고, 다음과 같이 API를 디자인하기로 하였습니다.:

apollo.addClass(elem, 'myclass');

물론 좋은 클래스 조작 모듈이라면 hasClass, addClass, removeClass 그리고 toggleClass 매소드를 가지고 있어야겠지요. 이 모든 메소드는 “apollo” 네임스페이스에서부터 시작되어 사용됩니다.

위에 있는 “addClass” 메소드를 자세히 살펴보면, 첫 번째 argument로 엘르먼트를 건네주는 것을 확인하실 수 있습니다. 하나의 커다란 커스텀 Object에 묶여서 사용되는 jQuery와는 다르게, 이 모듈은 하나의 DOM 엘르먼트를 받게 되는데, 이 엘르먼트를 기본 메소드를 쓰든 혹은 선택자 모듈을 사용해서 건네주든 그건 개발자 마음입니다. 두 번째 argument는 String 값으로 개발자가 원하는 클래스 이름을 던져주면 됩니다.

아래는 제가 만들고자 했던 모든 클래스 조작 매소드들로, 다음과 같은 사용 형태를 띠게 됩니다.:

apollo.hasClass(elem, 'myclass');
apollo.addClass(elem, 'myclass');
apollo.removeClass(elem, 'myclass');
apollo.toggleClass(elem, 'myclass');

자, 그럼 어디서부터 시작해야 할까요? 먼저, 모든 메소드를 갖다 붙일 Object가 필요하고, 또 모든 내부적 작동과 변수/매소드들을 포함할 function closure를 구현해야 할 겁니다. immediate-invoked function expression(IIFE)을 써서, apollo라는 이름의 Object를 감싸고 classList 기능을 구현하는 몇 개의 매소드들을 포함해서 모듈을 정의해 보았습니다.

(function() {
  var apollo = {};
 
  apollo.hasClass = function(elemclassName) {
    return elem.classList.contains(className);
  };
 
  apollo.addClass = function(elemclassName) {
    elem.classList.add(className);
  };
 
  apollo.removeClass = function(elemclassName) {
    elem.classList.remove(className);
  };
 
  apollo.toggleClass = function(elemclassName) {
    elem.classList.toggle(className);
  };
 
  window.apollo = apollo;
 
})();
 
apollo.addClass(document.body, 'test');

이제 classList 기능 구현이 완성되었으니, 오래된 버전의 브라우저 지원을 고려해 볼 차례죠. 간단히 말해, apollo 모듈의 목적은 브라우저에 상관 없이 클래스 조작에 대한 작지만 독립적이고 일관된 API를 가지고 기능을 구현하는 것입니다. 여기엔 아주 간단한 기능 탐지 기법이 유용하게 쓰일 수 있습니다.

classList 기능의 존재 여부를 판단하는 쉬운 방법은 다음과 같은 것이 있습니다.:

if ('classList' in document.documentElement) {
  // 기능을 지원함 
}

in 연산자를 써서 classList 기능의 존재 여부를 Boolean 값으로 확인할 수 있습니다. 다음 단계는 조건부로 우선 classList를 지원하는 브라우저에게만 API 제공하는 것이지요.:

(function() {
 
  var apollo = {};
  var hasClass, addClass, removeClass, toggleClass;
 
  if ('classList' in document.documentElement) {
    hasClass = function() {
      return elem.classList.contains(className);
    };
    addClass = function(elemclassName) {
      elem.classList.add(className);
    };
    removeClass = function(elemclassName) {
      elem.classList.remove(className);
    };
    toggleClass = function(elemclassName) {
      elem.classList.toggle(className);
    };
  }
 
  apollo.hasClass = hasClass;
  apollo.addClass = addClass;
  apollo.removeClass = removeClass;
  apollo.toggleClass = toggleClass;
 
  window.apollo = apollo;
 
})();

오래된 브라우저를 지원하는 방법은 여러 가지가 있는데, className String을 읽고 나서 모든 이름을 루프로 돌려가며 대체하거나 추가하는 등의 과정을 예로 들 수 있습니다. jQuery가 이런 코드 형태의 복잡한 과정을 거치는데, 전 이런 복잡한 과정을 적용해서 저의 새롭고 단순한 모듈을 불필요하게 부풀리고 싶지 않았습니다. 그래서 그냥 정규식을 써서 더 추가적인 비용 없이 비슷한 작업을 구현하였습니다.

아래는 이렇게 해서 제가 도출해 낸 가장 깔끔한 구현 방법입니다.:

function hasClass(elemclassName) {
  return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className);
}
 
function addClass(elemclassName) {
  if (!hasClass(elem, className)) {
    elem.className += (elem.className ? ' ' : '') + className;
  }
}
 
function removeClass(elemclassName) {
  if (hasClass(elem, className)) {
    elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'), '');
  }
}
 
function toggleClass(elemclassName) {
  (hasClass(elem, className) ? removeClass : addClass)(elem, className);
}

그럼 이것을 모듈에 적용해서, 미지원 브라우저를 위해 else 구문에 추가해줍니다.:

(function() {
 
  var apollo = {};
  var hasClass, addClass, removeClass, toggleClass;
 
  if ('classList' in document.documentElement) {
    hasClass = function() {
      return elem.classList.contains(className);
    };
    addClass = function(elemclassName) {
      elem.classList.add(className);
    };
    removeClass = function(elemclassName) {
      elem.classList.remove(className);
    };
    toggleClass = function(elemclassName) {
      elem.classList.toggle(className);
    };
  } else {
    hasClass = function(elemclassName) {
      return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className);
    };
    addClass = function(elemclassName) {
      if (!hasClass(elem, className)) {
        elem.className += (elem.className ? ' ' : '') + className;
      }
    };
    removeClass = function(elemclassName) {
      if (hasClass(elem, className)) {
        elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'), '');
      }
    };
    toggleClass = function(elemclassName) {
      (hasClass(elem, className) ? removeClass : addClass)(elem, className);
    };
  }
 
  apollo.hasClass = hasClass;
  apollo.addClass = addClass;
  apollo.removeClass = removeClass;
  apollo.toggleClass = toggleClass;
 
  window.apollo = apollo;
 
})();

지금까지의 작업은 jsFiddle에서 확인하실 수 있습니다.

이제 원래 구상했던 모든 기능을 구현했으므로, 여기서 마무리 짓겠습니다. apollo 모듈은 한꺼번에 여러 개의 클래스를 추가하는 등의 몇 가지 추가 기능들이 있는데, 더 관심이 있으시다면 여기에서 확인할 수 있습니다.

자, 그럼 지금까지 과정을 다시 짚어볼까요? 하나의 독립된 단위의 코드를 작성했는데 이놈은 오로지 한 가지의 작업에만 전념해서 그 기능을 잘 수행하게 하였습니다. 이런 모듈은 매우 단순해서 그냥 훑어봐도 이해하기 쉬우며 수정도 간편해서 유닛 테스트를 통한 기능 유효성 체크도 쉽습니다. 또한, jQuery가 필요치 않은 프로젝트에는 그냥 이 작은 덩치의 apollo 모듈만 불러오면 해당 기능을 충분히 활용할 수 있을 겁니다.

의존성 관리: AMD와 CommonJS

모듈의 개념은 새로운 게 아니어서, 우리는 항상 사용하고 있습니다. 이미 JavaScript는 브라우저를 통해야만 하는 것이 아니어서 서버 그리고 심지어 TV에서도 실행된다는 걸 아실 겁니다.

새로운 모듈로서 어떠한 형태의 것을 선택해서 사용해야 할까요? 그리고 또 어느 환경에서 사용할 수 있을까요? 고려해 볼 수 있는 게 “AMD”와 “CommonJS”라 불리는 두 가지의 개념이 있는데, 아래에서 살펴보도록 하겠습니다.

AMD

Asynchronous Module Definition(보통 AMD라 칭함)는 비동기식으로 읽히는 모듈을 정의하는 JavaScript API로서, 동기식으로 읽힘으로써 생기는 성능상의 비용과 함께 사용성, 디버깅 그리고 크로스 도메인 접근 문제 등을 피하는 수단으로 보통 브라우저에서 실행됩니다. AMD는 개발상 도움을 제공하는 동시에, 서러 다른 파일에 있는 JavaScript 모듈의 독립성을 보장해줍니다.

AMC는 define이라 불리는 function을 사용하는데, 모듈 자신과 함께 다른 export Objects를 정의합니다. 또한 AMD를 가지고 의존성을 지닌 임포트할 다른 모듈을 지정할 수도 있습니다. 아래는 AMD GitHub 프로젝트에 예시된 간단한 사용법입니다.:

define(['alpha'], function(alpha) {
  return {
    verbfunction() {
      return alpha.verb() + 2;
    }
  };
});

만약에 AMD 용법을 apollo에 적용한다면 다음과 같이 사용할 수 있습니다.:

define(['apollo'], function(alpha) {
  var apollo = {};
  var hasClass, addClass, removeClass, toggleClass;
 
  if ('classList' in document.documentElement) {
    hasClass = function() {
      return elem.classList.contains(className);
    };
    addClass = function(elemclassName) {
      elem.classList.add(className);
    };
    removeClass = function(elemclassName) {
      elem.classList.remove(className);
    };
    toggleClass = function(elemclassName) {
      elem.classList.toggle(className);
    };
  } else {
    hasClass = function(elemclassName) {
      return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className);
    };
    addClass = function(elemclassName) {
      if (!hasClass(elem, className)) {
        elem.className += (elem.className ? ' ' : '') + className;
      }
    };
    removeClass = function(elemclassName) {
      if (hasClass(elem, className)) {
        elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'), '');
      }
    };
    toggleClass = function(elemclassName) {
      (hasClass(elem, className) ? removeClass : addClass)(elem, className);
    };
  }
 
  apollo.hasClass = hasClass;
  apollo.addClass = addClass;
  apollo.removeClass = removeClass;
  apollo.toggleClass = toggleClass;
 
  window.apollo = apollo;
});

CommonJS

Node.js는 최근 몇 년 동안에 많은 주목을 받음과 동시에 의존성 관리 도구와 그 형태 또한 관심의 대상이 되고 있습니다. Node.js는 CommonJS라 불리는 것을 채용하고 있는데, 이것은 모듈의 내용을 정의할 때 “exports” Object를 사용하고 있습니다. 정말 단순하게 CommonJS를 구현해 놓은 예는 아래와 같습니다. (다른 곳에 사용될 것을 “export 한다”는 의미가 담겨 있습니다.)

// someModule.js 
exports.someModule = function () {
  return "foo";
};

위 코드는 자신의 모듈 파일 안에서 작성되어 집니다. 코드에선 someModue.js라는 이름을 가지고 있습니다. 다른 곳에서 해당 모듈을 가져와서 사용하려면, CommonJS에선 “require”라는 이름의 함수를 사용해서 지정해 놓으면 해당하는 각 의존 모듈을 불러오게 됩니다.:

// 'myModule'에 불러와서 사용 
var myModule = require('someModule');

만약에 Grunt나 Gulp를 써보셨다면, 많이 눈에 익은 형태일 겁니다.

이 형태를 apollo에도 적용해 본다면 다음과 같이 작성할 수 있는데, window 대신에 exports Object에다 모듈을 붙이게 됩니다. (코드 마지막에 있는 exports.apollo = apollo):

(function() {
 
  var apollo = {};
  var hasClass, addClass, removeClass, toggleClass;
 
  if ('classList' in document.documentElement) {
    hasClass = function() {
      return elem.classList.contains(className);
    };
    addClass = function(elemclassName) {
      elem.classList.add(className);
    };
    removeClass = function(elemclassName) {
      elem.classList.remove(className);
    };
    toggleClass = function(elemclassName) {
      elem.classList.toggle(className);
    };
  } else {
    hasClass = function(elemclassName) {
      return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className);
    };
    addClass = function(elemclassName) {
      if (!hasClass(elem, className)) {
        elem.className += (elem.className ? ' ' : '') + className;
      }
    };
    removeClass = function(elemclassName) {
      if (hasClass(elem, className)) {
        elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'), '');
      }
    };
    toggleClass = function(elemclassName) {
      (hasClass(elem, className) ? removeClass : addClass)(elem, className);
    };
  }
 
  apollo.hasClass = hasClass;
  apollo.addClass = addClass;
  apollo.removeClass = removeClass;
  apollo.toggleClass = toggleClass;
 
  exports.apollo = apollo;
 
})();

Universal Module Definition (UMD)

AMD와 CommonJS 모두 다 훌륭한 접근 방법이지만, 만약에 모든 환경에서 적용할 수 있는 모듈을 작성하고 싶다면 어떻게 해야 할까요? AMD, CommonJS 그리고 브라우저 모두 다 포함해서 말입니다.

처음엔 그냥 ifelse 구문을 사용해서 AMD 혹은 CommonJS를 지원하는지 확인해서 해당 형식에 맞은 함수를 그냥 단순하게 전달하는 방법을 사용했었습니다. 이런 아이디어는 나중에 보편적 해법으로 “UMD“라는 이름으로 적용되어 사용되고 있습니다. 이놈은 그냥 과거 if/else 기법을 한데로 묶어서 우린 그냥 하나의 함수를 전달만 해주면 각 모듈 형식에 맞게 알아서 처리해 줍니다. 아래는 프로젝트 저장소에 나와 있는 사용 예입니다.:

(function (rootfactory) {
  if (typeof define === 'function' && define.amd) {
      // AMD. Register as an anonymous module. 
      define(['b'], factory);
  } else {
      // Browser globals 
      root.amdWeb = factory(root.b);
  }
}(this, function (b) {
  //use b in some fashion. 
 
  // Just return a value to define the module export. 
  // This example returns an object, but the module 
  // can return a function as the exported value. 
  return {};
}));

맙소사! 이게 무슨 소리냐. 여기선 IIFE 블락의 두번째 인자에다가 우리의 함수를 던져주면, factory라는 이름의 로컬 변수에 지정되고 상황에 따라 자동으로 AMD 혹은 브라우저 전역 객체에 지정되어 집니다. 물론, 이놈은 CommonJS를 지원하지 않습니다만, 다음과 같은 약간의 코드 수정으로 이 문제를 해결할 수 있답니다. (해석을 쉽게 하기 위해 이번엔 코멘트도 삭제하였습니다.):

(function(rootfactory) {
  if (typeof define === 'function' && define.amd) {
    define(['b'], factory);
  } else if (typeof exports === 'object') {
    module.exports = factory;
  } else {
    root.amdWeb = factory(root.b);
  }
}(this, function(b) {
  return {};
}));

여기서 중요한 곳은 module.exports = factory 부분인데, 우리의 모듈을 상징하는 factory를 CommonJS에 붙여 줍니다.

지금까지 설명된 것을 정리하는 차원에서 제 apollo를 가지고 UMD에 맞게 설정해서 CommonJS 환경이나 AMD 그리고 브라우저에서도 사용될 수 있도록 고쳐봅시다! 아래에서는 GitHub에 저장된 최신 버전의 apollo 스크립트를 모두 옮겨 놓은 것입니다. 참고로 몇 가지 기능이 더 추가되어서 지금까지 설명했던 것보다 약간 더 복잡해 보일 수도 있습니다.:

/*! apollo.js v1.7.0 | (c) 2014 @toddmotto | https://github.com/toddmotto/apollo */
(function(rootfactory) {
  if (typeof define === 'function' && define.amd) {
    define(factory);
  } else if (typeof exports === 'object') {
    module.exports = factory;
  } else {
    root.apollo = factory();
  }
})(this, function() {
 
  'use strict';
 
  var apollo = {};
 
  var hasClass, addClass, removeClass, toggleClass;
 
  var forEach = function(itemsfn) {
    if (Object.prototype.toString.call(items) !== '[object Array]') {
      items = items.split(' ');
    }
    for (var i = 0; i < items.length; i++) {
      fn(items[i], i);
    }
  };
 
  if ('classList' in document.documentElement) {
    hasClass = function(elemclassName) {
      return elem.classList.contains(className);
    };
    addClass = function(elemclassName) {
      elem.classList.add(className);
    };
    removeClass = function(elemclassName) {
      elem.classList.remove(className);
    };
    toggleClass = function(elemclassName) {
      elem.classList.toggle(className);
    };
  } else {
    hasClass = function(elemclassName) {
      return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className);
    };
    addClass = function(elemclassName) {
      if (!hasClass(elem, className)) {
        elem.className += (elem.className ? ' ' : '') + className;
      }
    };
    removeClass = function(elemclassName) {
      if (hasClass(elem, className)) {
        elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'), '');
      }
    };
    toggleClass = function(elemclassName) {
      (hasClass(elem, className) ? removeClass : addClass)(elem, className);
    };
  }
 
  apollo.hasClass = function(elemclassName) {
    return hasClass(elem, className);
  };
 
  apollo.addClass = function(elemclasses) {
    forEach(classes, function(className) {
      addClass(elem, className);
    });
  };
 
  apollo.removeClass = function(elemclasses) {
    forEach(classes, function(className) {
      removeClass(elem, className);
    });
  };
 
  apollo.toggleClass = function(elemclasses) {
    forEach(classes, function(className) {
      toggleClass(elem, className);
    });
  };
 
  return apollo;
 
});

지금까지 제가 작성한 모듈을 가지고 다양한 환경에서도 잘 작동할 수 있도록 말 그대로 다시 포장해 보았습니다. 작업 환경에 이런 새로운 의존성 관리 기법을 적용한다면 더 나은 융통성을 발휘할 수 있을 겁니다. 물론 이것은 JavaScript 라이브러리가 처음부터 작은 단위의 함수 모음으로 쪼개어 개발되지 않았다면 불가능한 일이었겠지요.

테스팅

일반적으로 모듈은 작은 단위의 유닛 테스트와 함께 배포되는데, 이렇게 하면 커다란 라이브러리와는 비교되게 다른 개발자가 프로젝트에 참여해서 오류나 기능 개선 요구하기도 훨씬 쉬워집니다. 그래서 커다란 라이브러리의 경우엔 새로운 기능의 추가나 오류 수정이 비교적 오래 걸리는 반면에, 작은 모듈은 빠른 속도로 자주 갱신되는 경향을 보입니다.

마무리

직접 모듈을 작성하고 다양한 개발 환경을 지원한다면 수많은 개발자에게 도움을 줄 수 있을 것이고, 이것은 아주 멋진 경험일 겁니다. 또한, 개발을 더 재밌고 관리하기도 편하며, 매일 사용하는 도구를 더 깊이 이해할 기회입니다. 보통 모듈과 함께 배포되는 문서를 참고하면 빠르고 쉽게 바로 자기만의 작업 환경에 도입해서 사용하실 수 있을 겁니다. 만약에 모듈이 적합한 것이 아니라면, 다른 것을 찾아보거나 혹은 직접 작성하실 수도 있겠지요. 이것이야말로 모듈이 가지고 있는 커다란 장점으로, 보통 하나의 거대 용량의 라이브러리에 묶여서 거의 모든 것을 의존하게 되는 상황에선 분명 쉽게 해결할 수 없는 문제입니다.

보너스: ES6 모듈

마무리 짓기 전에 덧붙이고 싶은 말이 있다면, 종전의 JavaScript 라이브러리들이, 예를 들어 클래스 조작과 같이, 원래의 랭귀지 자체에 어떤 영향을 끼치게 되는 현상을 살펴보면 흥미롭지 않으세요? 이번에도 역시나, 다음 세대의 JavaScript 랭귀지인 ES6에서도 또 다시 같은 현상이 일어나고 말았습니다! 랭귀지 자체에서 imports와 exports를 지원하게 된 것입니다!

모듈을 export 하는 법.:

/// myModule.js 
function myModule () {
  // module content 
}
export myModule;

이에 대응해서 import 하는 법.:

import {myModule} from 'myModule';

모듈 규약을 포함해서 자세한 ES6에 관한 내용은 여기에서 확인하실 수 있습니다.

]]>
https://appletree.or.kr/blog/web-development/javascript/modules-%ec%95%9e%ec%9c%bc%eb%a1%9c-javascript-%eb%9d%bc%ec%9d%b4%eb%b8%8c%eb%9f%ac%eb%a6%ac%ea%b0%80-%eb%82%98%ec%95%84%ea%b0%88-%ea%b8%b8/feed/ 0
혼동하기 쉬운 module.exports와 exports의 차이. https://appletree.or.kr/blog/web-development/javascript/%ed%98%bc%eb%8f%99%ed%95%98%ea%b8%b0-%ec%89%ac%ec%9a%b4-module-exports%ec%99%80-exports%ec%9d%98-%ec%b0%a8%ec%9d%b4/ https://appletree.or.kr/blog/web-development/javascript/%ed%98%bc%eb%8f%99%ed%95%98%ea%b8%b0-%ec%89%ac%ec%9a%b4-module-exports%ec%99%80-exports%ec%9d%98-%ec%b0%a8%ec%9d%b4/#respond Thu, 29 May 2014 02:35:45 +0000 http://appletree.or.kr/blog/?p=2613 node module을 처음 접해서 사용하다 보면 많은 사람이 갖게 되는 질문인데, 다음 글에 간단명료하게 정의되어 있다.

Difference between “module.exports” and “exports” in the CommonJs Module System – Stack Overflow

결국, 다음 코드로 간단히 설명된다.

var module = { exports: {} };
var exports = module.exports;
 
// your code 
 
return module.exports;

exports의 property에 설정된 것은 문제없지만, exports 자체를 다른 것으로 설정해서 바꿔버리면 module.exports와의 연결고리가 끊어지게 되므로 이점 주의해야 한다.

좀 더 자세히 설명해놓은 글: Node.js Module – exports vs module.exports

module.exports.*exports.* 모두 다 함께 쓸 수 있다고 해서 번갈아 혼합해서 쓸 필요는 없고, module.exports.*의 존재 자체는 이해하되 일관성 있게 exports.*를 애용해서 사용해 줄 것을 권장한다.

]]>
https://appletree.or.kr/blog/web-development/javascript/%ed%98%bc%eb%8f%99%ed%95%98%ea%b8%b0-%ec%89%ac%ec%9a%b4-module-exports%ec%99%80-exports%ec%9d%98-%ec%b0%a8%ec%9d%b4/feed/ 0
특정 function을 n milliseconds 동안 최대 한 번만 실행시키기 https://appletree.or.kr/blog/web-development/javascript/%ed%8a%b9%ec%a0%95-function%ec%9d%84-n-milliseconds-%eb%8f%99%ec%95%88-%ec%b5%9c%eb%8c%80-%ed%95%9c-%eb%b2%88%eb%a7%8c-%ec%8b%a4%ed%96%89%ec%8b%9c%ed%82%a4%ea%b8%b0/ https://appletree.or.kr/blog/web-development/javascript/%ed%8a%b9%ec%a0%95-function%ec%9d%84-n-milliseconds-%eb%8f%99%ec%95%88-%ec%b5%9c%eb%8c%80-%ed%95%9c-%eb%b2%88%eb%a7%8c-%ec%8b%a4%ed%96%89%ec%8b%9c%ed%82%a4%ea%b8%b0/#respond Mon, 05 May 2014 10:47:26 +0000 http://appletree.or.kr/blog/?p=2609
Function.prototype.throttle = function(n) {
  var fn = this;
  var throttled = false;
  return function() {
    if (throttled) return;
    throttled = true;
    fn.apply(null, arguments);
    setTimeout(function() {
      throttled = false;
    }, n);
  };
};

가령 window.onscroll event에 심어놓은 function이 불필요하게 너무 자주 실행되는 것을 방지하려는 목적으로 사용될 수 있는데, 간단한 사용법으로 다음과 같이 적용할 수 있다.

var logTest = function() {
  console.log('test');
};
 
window.onscroll = logTest.throttle(1000);

이렇게 해놓으면, 최대 1초에 한 번씩만 등록된 함수가 실행되면서 콘솔에 해당 메시지가 찍히는 것을 볼 수 있다. 비슷한 함수로 Underscore.jsLo-Dash에서도 모두 throttle이란 이름의 함수를 함께 제공하고 있음.

따온 곳: Functional Javascript Tools & Tricks – Tim Ruffles on Vimeo

]]>
https://appletree.or.kr/blog/web-development/javascript/%ed%8a%b9%ec%a0%95-function%ec%9d%84-n-milliseconds-%eb%8f%99%ec%95%88-%ec%b5%9c%eb%8c%80-%ed%95%9c-%eb%b2%88%eb%a7%8c-%ec%8b%a4%ed%96%89%ec%8b%9c%ed%82%a4%ea%b8%b0/feed/ 0
JavaScript timers의 지연 시간을 무력화시키는 방법 https://appletree.or.kr/blog/web-development/javascript/javascript-timers%ec%9d%98-%ec%a7%80%ec%97%b0-%ec%8b%9c%ea%b0%84%ec%9d%84-%eb%ac%b4%eb%a0%a5%ed%99%94%ec%8b%9c%ed%82%a4%eb%8a%94-%eb%b0%a9%eb%b2%95/ https://appletree.or.kr/blog/web-development/javascript/javascript-timers%ec%9d%98-%ec%a7%80%ec%97%b0-%ec%8b%9c%ea%b0%84%ec%9d%84-%eb%ac%b4%eb%a0%a5%ed%99%94%ec%8b%9c%ed%82%a4%eb%8a%94-%eb%b0%a9%eb%b2%95/#respond Thu, 06 Mar 2014 04:56:39 +0000 http://appletree.or.kr/blog/?p=2599 가끔 웹페이지에 걸려있는 링크를 타고 들어가 보면 타이머를 걸어 놓고서 일정 시간이 지난 후에야, 가령 특정 form의 자료 입력과 같은, 작업을 진행할 수 있게 해 놓은 곳과 마주치곤 한다. 보통 링크 클릭을 통한 광고와 수익의 창출을 목적으로 이런 현상이 퍼지는데, 이럴 때마다 불편함과 함께 은근 짜증을 일으키게 한다.

그래서 이런 때를 대비해서 아래와 같은 JavaScript timers의 지연 시간을 아주 짧게 써는 실험 목적의 주문을 만들어 봤다. 주문이 잘 먹히면 더딤 없는 쾌속 진행의 길을 뚫을 수 있다. (물론, 사이트에서 어떤 추가 확인장치를 해 놓은 곳이라면 이것도 별 소용이 없겠지만.)

var originalInterval = this.setInterval, originalTimeout = this.setTimeout;
this.setInterval = function(funcdelay) {
  originalInterval(func, 10);
};
this.setTimeout = function(funcdelay) {
  originalTimeout(func, 10);
};

이를 이용한 간편 사용 목적의 bookmarklet: fastTimer – 가속기가 달린 고장난(?) Timer.

]]>
https://appletree.or.kr/blog/web-development/javascript/javascript-timers%ec%9d%98-%ec%a7%80%ec%97%b0-%ec%8b%9c%ea%b0%84%ec%9d%84-%eb%ac%b4%eb%a0%a5%ed%99%94%ec%8b%9c%ed%82%a4%eb%8a%94-%eb%b0%a9%eb%b2%95/feed/ 0
CSS Variables와 Calc() function을 써서 rem unit의 fallback 구현하기 https://appletree.or.kr/blog/web-development/css/css-variables%ec%99%80-calc-function%ec%9d%84-%ec%8d%a8%ec%84%9c-rem-unit%ec%9d%98-fallback-%ea%b5%ac%ed%98%84%ed%95%98%ea%b8%b0/ https://appletree.or.kr/blog/web-development/css/css-variables%ec%99%80-calc-function%ec%9d%84-%ec%8d%a8%ec%84%9c-rem-unit%ec%9d%98-fallback-%ea%b5%ac%ed%98%84%ed%95%98%ea%b8%b0/#respond Tue, 07 Jan 2014 02:14:07 +0000 http://appletree.or.kr/blog/?p=2582 CSS3에서 새로 소개된 rem(root em) unit은 em unit과 마찬가지로 상대성을 띤 Relative length units 중 하나지만, 기존 부모 요소를 기준으로 한 em unit과 달리 root element, 그러니까 html element를 기준으로 그 크기 값이 정해지면서 부모 요소의 중첩으로 생기는 뻥튀기의 부작용을 걱정할 필요가 없다는 것이 장점이다.

그래서, 어느 하나의 요소를 기준으로 또 다른 요소의 상대적 크기를 정해주고 싶을 때 많이 사용되는데, 문젠 언제나 그렇듯 IE < 9을 포함해서 아직 만족스럽지 못한 rem unit의 브라우저 지원 상황을 고려해서, 미지원 브라우저에서도 인식되도록 적절한 대응 값을 함께 정의해 줘야 하는 성가심이 있다.

이런 상황에서 SaasLESS 혹은 Stylus와 같은 CSS pre-processor의 힘을 빌린다면 효율적인 CSS 작성에 큰 도움이 된다는 것은 이미 잘 알려져 있다. 하나의 좋은 본보기로, CSS pre-processor를 써서 rem unit의 fallback을 구현한 예.

한편 아래는 실험 삼아 지금 한창 논의되고 있는 CSS VariablesCalc() function만을 써서 rem unit의 fallback을 구현해 보았는데, 여기에 꼭 안성맞춤으로 CSS polyfill임을 자처하면서 지금 한창 개발중인 또 하나의 CSS preprocessor인 Myth의 힘을 빌려서 바로 적용해 보았다.

:root {
  var-base-font-size: 10;
  var-body-font-size: 14;
  var-heading1-font-size: 18;
}
 
html {
  font-size: calc(var(base-font-size) / 16 * 100%);
}
 
body {
  font-size: calc(0px + var(body-font-size));
  font-size: calc(0rem + var(body-font-size) / var(base-font-size));
}
 
h1 {
  font-size: calc(0px + var(heading1-font-size));
  font-size: calc(0rem + var(heading1-font-size) / var(base-font-size));
}

이렇게 작성된 CSS는 Myth를 통해 다음과 같이 변환된다.

html {
  font-size: 62.5%;
}
 
body {
  font-size: 14px;
  font-size: 1.4rem;
}
 
h1 {
  font-size: 18px;
  font-size: 1.8rem;
}

물론 여기선 기존 CSS pre-processors가 제공하는 mixin 기능을 쓸 수가 없으므로 CSS를 작성할 때 공식을 계산하는 부분에서 추가 수정 작업이 필요하겠지만, 어차피 중복되는 공통 부분은 class로 빼서 적용해주면 될 것이고, 이렇게 한번 작성하고 나면 root element에서 설정된 변숫값 한 곳만 바꿔주면 해당 변수를 사용한 다른 연관된 property의 value도 자동 갱신되기 때문에 디자인 변경 작업이 훨씬 수월해진다.

이렇게 CSS variables와 calc() function은 기존 CSS 작성에 더 많은 효율을 안겨다 줄 터인데, 브라우저들의 지원 상황에 개의치 않고 표준 문법 그대로 지금 당장 여기서 논의된 여러 기능을 바로 적용해서 맛볼 수 있다는 것이 Myth가 가진 가장 큰 장점이자 매력이라 생각된다. 다만, CSS pre-processors이기 때문에 갖는 한계로서 CSS variables는 cascade를 지원하지 않으며 또 Math 기능은 브라우저의 runtime interpolation 과정, 그러니까 웹 페이지를 다 읽은 후 CSS를 재해석해서 처리하는 과정을 그대로 적용할 수 없음으로써 생기는 약간의 사용 제약이 있다.

덤으로, 아래는 Myth가 지원하는 색채 조작(Color Manipulation)과 접두어 자동 생성 기능(No Prefixes)을 사용한 예.

:root {
  var-warning-font-color: gold;
}
 
.warning {
  font-weight: bold;
  color: color(var(warning-font-color) tint(10%));
  background: linear-gradient(to bottombrowndarkorange);
  border-radius: 6px;
}
 
/* output */
.warning {
  font-weight: bold;
  color: rgb(255, 219, 25);
  background: -webkit-gradient(linear, left topleft bottom, from(brown), to(darkorange));
  background: -webkit-linear-gradient(topbrowndarkorange);
  background: linear-gradient(to bottombrowndarkorange);
  border-radius: 6px;
}

이렇듯, 앞으로 시간이 지나 브라우저 지원 상황만 개선된다면 CSS 작성이 한결 수월해지리라 기대해 본다.

]]>
https://appletree.or.kr/blog/web-development/css/css-variables%ec%99%80-calc-function%ec%9d%84-%ec%8d%a8%ec%84%9c-rem-unit%ec%9d%98-fallback-%ea%b5%ac%ed%98%84%ed%95%98%ea%b8%b0/feed/ 0
JavaScript function의 default parameters 설정 법 https://appletree.or.kr/blog/web-development/javascript/javascript-function%ec%9d%98-default-parameters-%ec%84%a4%ec%a0%95-%eb%b2%95/ https://appletree.or.kr/blog/web-development/javascript/javascript-function%ec%9d%98-default-parameters-%ec%84%a4%ec%a0%95-%eb%b2%95/#respond Fri, 06 Dec 2013 02:51:45 +0000 http://appletree.or.kr/blog/?p=2573 보통 JavaScript function을 작성하다 보면, 경우에 따라 딱히 정해져 있지 않은 수의 parameters를 받아서 처리해야 할 때가 있다. 이럴 땐, 아래 코드처럼 전달된 각 parameter의 타입을 확인해서 전달받지 못한 놈은 미리 정해진 기본값을 지정해서 사용하는 패턴이 많이 쓰인다.

function foo(ab) {
  a = typeof a !== 'undefined' ? a : 'default_a';
  b = typeof b !== 'undefined' ? b : 'default_b';
  ...
}

이런 사용 패턴의 쓰임새가 점점 늘어남에 따라, ECMAScript 6에선 parameters에 default values를 아주 쉽게 지정할 수 있는 용법이 제안되었는데, 그 사용 예를 들면 다음과 같다.

/* ECMAScript 6에 포함된 Default Function Parameters */
function multiply(ab = 1) {
  return a * b;
}
 
multiply(5); // 5 

참고로, ES6의 default function params 웹브라우저 지원 상황을 보면, Firefox(v.15 이상)에서만 지원해서 아직 실제로 사용하기엔 이른 아쉬움이 있다.

그런데 이렇게 function에 전달할 parameters의 개수가 가령 세 개 이상으로 늘어나면 이에 대응하는 각 parameter의 타입을 확인하는 과정도 늘어날 수밖에 없고, 또 전달된 parameters의 순서도 그 결과 값에 중대한 영향을 미치게 된다. 이는 실제 function 사용 시 parameter의 순서를 착각해서 다르게 전달한다면 커다란 문제를 일으키게 될 소지가 있으므로 주의해야 한다.

그래서, 이와 같은 사용의 번거로움과 실수를 방지하려고 전달할 parameters의 수가 많을 땐 다음과 같이 function parameter로 하나의 options object를 받아서 구현하는 방법도 널리 쓰이고 있다.

function fooWithOptionsObject(options) {
  options = options || {};
  var defaults = {option1: 'default_1', option2: 'default_2', option3: 'default_3'};
 
  for (var prop in defaults)  {
    options[prop] = typeof options[prop] !== 'undefined' ? options[prop] : defaults[prop];
  }
  console.log(options.option1, options.option2, options.option3);
}
 
var params = {option1: 'myOption_1', option2: 'myOption_2'};
fooWithOptionsObject(params);
// → myOption_1 myOption_2 default_3 

이렇게 하면, 함수를 쓸 때마다 전달 parameters의 순서를 신경 쓸 필요없이 그냥 지정하고 싶은 parameter의 값을 해당 object key의 value에다 심어서 전달해주면 되고, 나머진 자동으로 default 값이 지정되는 장점이 있다.

한편, 위 함수의 parameter로 전달된 options object에 적용된 과정을 살펴보면, 원래의 object와 또 하나의 기준 object를 비교해서 원래 전달된 object에 정의되지 않은(undefined) properties를 기준 object의 것으로 채워서 다시 원래의 object를 되돌려주는 것이라 볼 수 있는데, 이와 같은 기능은 대표적인 jQuery.extend() method와 더불어 일반 다목적 utilities function library인 Underscore.jsLo-Dash에서도 _.defaults라는 이름의 함수를 다 같이 제공하고 있으며 각각의 사용법은 다음과 같다.

var defaults = { validate: false, limit: 5, name: 'foo' };
var options = { validate: true, name: 'bar' };
 
$.extend({}, defaults, options);
_.defaults(options, defaults);
// →  { validate: true, limit: 5, name: 'bar'} 

JavaScript function parameter overloading 만세~!
용량을 초과해버린 짐차. 달리는 게 신기할 지경.
우연히도 JavaScript 관련 글을 올리면서 JavaScript의 18번째 생일을 맞이하게 되었군.
JavaScript가 점점 강력해지는 엔진을 달고서 어디까지 쭉쭉 내달리지 앞으로가 더 기대되는 시점이다.

]]>
https://appletree.or.kr/blog/web-development/javascript/javascript-function%ec%9d%98-default-parameters-%ec%84%a4%ec%a0%95-%eb%b2%95/feed/ 0
JavaScript Revealing Module Pattern https://appletree.or.kr/blog/web-development/javascript/javascript-revealing-module-pattern/ https://appletree.or.kr/blog/web-development/javascript/javascript-revealing-module-pattern/#respond Sun, 01 Sep 2013 04:38:44 +0000 http://appletree.or.kr/blog/?p=2555 JavaScript 실행 환경에서 global에 새로운 변수를 등록할 때는 또 다른 제3의 스크립트가 등록한 변수와의 있을지 모를 충돌을 피하려는 노력의 하나로 지극히 제한되게 그 사용을 최소화하도록 권장하고 있다. JavaScript의 기본 문법에는 Module을 구현하는 기능을 갖추고 있지 않아서 (ECMAScript 6th Edition, Harmony에선 Module System이 추가될 전망), 이를 비슷하게나마 구현하기 위한 여러 패턴이 소개됐으며 그 중 가장 널리 쓰이는 패턴이 바로 Rrevealing Module Pattern이다.

아래는 개인적으로 이 패턴에다 IIFE를 더 해서 쓰고 있는 snippet.

;(function(globaldocnameSpaceundefined) {
  'use strict';
 
  var privateVar = 1;
  var privateMethod = function(args) {
 
  };
 
  nameSpace.publicMethod = function(args) {
 
  };
 
  global.nameSpace = nameSpace;
}(this, this.document, this.nameSpace || {}));

JavaScript의 function scope와 closures 덕분에 function 안에 선언된 methods나 variables는 기본적으로 외부에 노출되지 않어서, 그 안의 또 다른 function 이외엔 접근할 수 없도록 차단되어 있는데, 그 중 원하는 것만 꼭 집어서 global에 하나로 공개된 자기만의 namespace가 가지고 있는 property에다 갖다 붙여준다. 이렇게 하면 global 환경에 공개된 기존의 variables와 methods 이름과의 충돌을 최소화할 수 있다.

맨 앞에 희한하게 붙어 있는 세미콜론(;)은 JavaScript 로딩 속도를 최소화하려고 압축해서 다른 스크립트 파일과 하나로 합치는 과정에서, 만약에 다른 스크립트 파일에 있는 마지막 statement 끝에 세미콜론이 빠져있을 경우, 합치는 과정에서 IIFE가 함수 arguments로 오인되는 일을 미리 방지하기 위한 것인데, 이와 관련된 자세한 내용의 글. – IIFE Leading semicolon

남의 밭에서 신발 끈 묶을 땐 항상 조심스럽게.

]]>
https://appletree.or.kr/blog/web-development/javascript/javascript-revealing-module-pattern/feed/ 0
웹페이지 로딩 후, 필요할 때에만 특정 JavaScript 파일을 읽어 들이는 방법 https://appletree.or.kr/blog/web-development/javascript/%ec%9b%b9%ed%8e%98%ec%9d%b4%ec%a7%80-%eb%a1%9c%eb%94%a9-%ed%9b%84-%ed%95%84%ec%9a%94%ed%95%a0-%eb%95%8c%ec%97%90%eb%a7%8c-%ed%8a%b9%ec%a0%95-javascript-%ed%8c%8c%ec%9d%bc%ec%9d%84-%ec%9d%bd%ec%96%b4/ https://appletree.or.kr/blog/web-development/javascript/%ec%9b%b9%ed%8e%98%ec%9d%b4%ec%a7%80-%eb%a1%9c%eb%94%a9-%ed%9b%84-%ed%95%84%ec%9a%94%ed%95%a0-%eb%95%8c%ec%97%90%eb%a7%8c-%ed%8a%b9%ec%a0%95-javascript-%ed%8c%8c%ec%9d%bc%ec%9d%84-%ec%9d%bd%ec%96%b4/#comments Sun, 25 Aug 2013 12:24:54 +0000 http://appletree.or.kr/blog/?p=2550 JavaScript 파일을 웹페이지에 추가하는 방법은 여러 가지가 있는데, 과거엔 주로 head tag에다 추가해 줄 때가 많았으나, 이렇게 하면 브라우저가 JavaScript 파일을 다 읽어 들이는 동안 웹페이지의 렌더링이 잠깐 멈추게 되는 부작용이 있어서, 최근엔 웹페이지를 되도록 빠르게 화면에 뿌려주려고 body tag 마지막에 얹어 주는 것을 권장하고 있다.

그런데 최근 날로 웹 애플리케이션의 개발과 그 활용 방법론이 많이 알려지면서 JavaScript 파일이 점점 더 커지는 경향이 있는데, 무조건 내려받게 되는 이런 JavaScript 파일의 용량을 조금이라도 줄이려는 노력도 따라서 필요해진다. 그래서 페이지에 얹어진 JavaScript 파일 중에 특정 기능이 빈번히 사용되지 않고 특별한 경우에만 쓰이는 것이라면, 이마저도 주요 JavaScript 파일에서 빼어내 특정 조건에 따라 필요할 때만 로딩해주는 기술이 쓰일 수 있다.

jQeury에선 이럴 때에 꼭 안성맞춤인 jQuery.getScript() 함수를 제공하고 있는데, 사용법은 다음과 같다.

$.getScript("ajax/test.js", function(datatextStatusjqxhr) {
   console.log(data); //data returned 
   console.log(textStatus); //success 
   console.log(jqxhr.status); //200 
   console.log('Load was performed.');
});

jQuery를 쓰지 않고 일반 JavaScript로 구현하려면 javascript_loader.js를 참고해서 구현해 볼 수도 있다.

웹페이지를 구성하는 주요 기술 중 JavaScript의 비중이 점점 더 커지는 것은 어쩔 수 없지만, 그렇다고 다른 기술의 영역까지 침범해서, 그에 따른 나중에 쓸데없이 들어가는 개발과 운용상의 소모적 낭비는 피해야 할 것이다.

이와 관련된 주제의 강연 영상: Enough with the JavaScript Already by Nicholas Zakas – YouTube

참고로, 특정 요소에만 사용되는 JavaScript 함수를 따로 로딩하려고 할 때 사용될 수도 있는 jQuery plugin으로 proximity-event가 있는데, 특정 요소에 마우스 커서가 정해진 사정거리 안으로 접근하면 그에 관한 event를 쏴줘서 여기에 등록된 함수를 자동 실행시켜주는 기능이 있다. 그런데 여기서 주의해야 할 점은, 가령 웹 Form의 특정 요소에 어떤 기능을 추가해 주고 싶을 때에는 접근성을 고려해서 focus event에도 이 함수 로딩 관련 JavaScript가 실행되었는지 확인해서, 로딩되지 않았다면 따로 이 추가 기능의 JavaScript 파일을 불러드리도록 처리해야 한다.

]]>
https://appletree.or.kr/blog/web-development/javascript/%ec%9b%b9%ed%8e%98%ec%9d%b4%ec%a7%80-%eb%a1%9c%eb%94%a9-%ed%9b%84-%ed%95%84%ec%9a%94%ed%95%a0-%eb%95%8c%ec%97%90%eb%a7%8c-%ed%8a%b9%ec%a0%95-javascript-%ed%8c%8c%ec%9d%bc%ec%9d%84-%ec%9d%bd%ec%96%b4/feed/ 2
blockquote 요소가 맞닥뜨린 위기 상황 https://appletree.or.kr/blog/web-development/html/blockquote-%ec%9a%94%ec%86%8c%ea%b0%80-%eb%a7%9e%eb%8b%a5%eb%9c%a8%eb%a6%b0-%ec%9c%84%ea%b8%b0-%ec%83%81%ed%99%a9/ https://appletree.or.kr/blog/web-development/html/blockquote-%ec%9a%94%ec%86%8c%ea%b0%80-%eb%a7%9e%eb%8b%a5%eb%9c%a8%eb%a6%b0-%ec%9c%84%ea%b8%b0-%ec%83%81%ed%99%a9/#comments Mon, 19 Aug 2013 06:27:43 +0000 http://appletree.or.kr/blog/?p=2542 blockquote element는 원래 다른 곳에 있는 소스 일부를 인용해서 표시할 때 사용되는 요소로, 인용한 해당 소스의 주소는 보통 cite attribute에 넣어서 표시해 주게 된다.

그런데 여기서 문제가 되는 점은 cite attribute에 포함된 소스는 기본적으로 해당 문서를 해석하는 user agents에게만 인식되고, 일반 사용자에겐 전혀 노출되지 않기 때문에, 이 정보를 제대로 표시해 주려면 CSS 혹은 JavaScript의 힘을 빌려 밖으로 끄집어내서 그냥 일반 텍스트로 보여줄 수밖에 없었다.

이런 바람직하지 않은 상황을 해결하려고 아래에 표시된 코드처럼 blockquote 요소 안에 <footer>를 써서 인용 소스를 표시해주는 방법이 제안되기도 했으나, 표준에 들어맞지 않는 사용으로 Spec Editor로부터 거부되기도 하였다. (참고로, 최근에 이를 다시 허용해 달라고 요구하는 제안이 보고되었음.)

<blockquote>
  <p>The <code><span class="pln">blockquote</span></code> element represents a section that is quoted from another source.</p>
  <footer>&mdash; <cite><a title="4.5 Grouping content &mdash; HTML5" href="http://dev.w3.org/html5/spec/grouping-content.html#the-blockquote-element">W3C HTML5 specification</a></cite></footer>
</blockquote>

대신 HTML 표준 문서의 blockquote element를 설명해 놓은 부분을 보면 이와 관련 권장되는 마크업 방법론으로 figure, figcaption 요소를 함께 쓴 다음과 같은 것이 예시되어 있다.

<figure>
 <blockquote>
  <p>The truth may be puzzling. It may take some work to grapple with.
  It may be counterintuitive. It may contradict deeply held
  prejudices. It may not be consonant with what we desperately want to
  be true. But our preferences do not determine what's true. We have a
  method, and that method helps us to reach not absolute truth, only
  asymptotic approaches to the truth — never there, just closer
  and closer, always finding vast new oceans of undiscovered
  possibilities. Cleverly designed experiments are the key.</p>
 </blockquote>
 <figcaption>Carl Sagan, in "<cite>Wonder and Skepticism</cite>", from
 the <cite>Skeptical Enquirer</cite> Volume 19, Issue 1 (January-February
 1995)</figcaption>
</figure>

결국, 이런 혼돈된 상황을 마무리 지으려는 조치로 아예 blockquote 요소를 spec에서 없애버리고 대신 figure 요소에 그 임무를 얹어주자는 제안이 나오게 된 상황이다.

이렇게 되면서, 인용 소스를 제대로 표시해주려면 blockquote 요소 안에 footer를 써서 표시하는 것이 허용되거나, 아예 blockquote의 임무를 figure 요소가 떠맡게 되는 결정이 나야 하는 상황인데, 두 주장의 타협점으로서 footer 요소 대신에 오로지 인용한 소스의 이름을 표시해주는 생소한 HTML3 시절의 <credit> element가 새로이 거론되기까지 하였다.

당장 blockquote 요소가 표준 명세서에서 사라질 것 같지는 않고 더 지켜봐야 할 상황인데, 항상 느끼지만 웹의 기본이자 밑바탕이 되는 HTML 마크업은 그 자유도가 커서 저자의 의도에 따라 다양한 방법론이 개발될 수 있는데, 그만큼 올바른 시멘틱 마크업의 결과물을 뽑아내기란 정말 어렵고 고려해야 할 사항도 많다는 것을 이번 사건이 다시 상기시켜주는 계기가 되었다.

– 내용 추가(8/23, 2013): HTML 5.1 Nightly Spec 문서의 cite element 설명 부분을 보면, 이젠 인용 문구의 원 소스를 나타내는 cite 요소에 사람의 이름도 쓸 수 있게 수정되었다.

– 내용 추가(8/29, 2013): HTML 5.1 Nightly Spec 문서의 blockquote element 설명 부분에는, 인용 소스를 표시해줄 때 다음과 같이 blockquote element 안에다 footer element로 감싸서 표시해 주는 예시가 추가되었다.

<blockquote>
 <p>I contend that we are both atheists. I just believe in one fewer
 god than you do. When you understand why you dismiss all the other
 possible gods, you will understand why I dismiss yours.</p>
 <footer>— <cite>Stephen Roberts</cite></footer>
</blockquote>

– 내용 추가(9/3, 2013): HTML 5.1 Nightly Spec 문서의 blockquote element 설명 부분에서, 인용구 안에 있는 인용 소스를 표시해줄 때는, 그동안 많이 쓰였던 마크업 패턴에 따라, 다음과 같이 cite element로 감싸서 표시해주는 예가 추가되었다.

<blockquote>
 The people recognize themselves in their commodities; they find their soul in their automobile, hi-fi set, split-level home, kitchen equipment.
 — <cite><a href="http://en.wikipedia.org/wiki/Herbert_Marcuse">Herbert Marcuse</a></cite>
</blockquote>

위 예시를 보면, 보통 인용구를 blockquote 요소로 감싸서 표시할 땐 해당 인용구를 무조건 p element로 감싸야만 되는 줄 알고 있었는데, short snippets 그러니까 짧은 단편적 인용구는, 그냥 p elements를 쓰지 않아도 된다는 사실을 처음 발견함. 약간 애매하단 생각이 듦.

]]>
https://appletree.or.kr/blog/web-development/html/blockquote-%ec%9a%94%ec%86%8c%ea%b0%80-%eb%a7%9e%eb%8b%a5%eb%9c%a8%eb%a6%b0-%ec%9c%84%ea%b8%b0-%ec%83%81%ed%99%a9/feed/ 2