Rev. 2.65

구글 검색 콘솔액셀러레이티드 모바일 페이지(AMP)라는 신기한 녀석이 출현했습니다. AMP는 모바일 환경에서 빠르게 로드되도록 설계된 페이지입니다. 자신의 웹사이트에 이를 적용하면 구글에서 제공하는 캐시에 저장되어 구글을 비롯한 여러 메타사이트가 웹페이지 프리뷰 등의 목적으로 활용할 수 있게 됩니다. 이전 글에 간단한 소개가 있으니 참고하세요.

AMP HTML 사양에 대한 기본적인 내용을 번역하기보다는 제가 이 작업을 수행하면서 경험한 삽질이나 알아낸 것, 유용하다고 생각되는 방법을 중심으로 설명하겠습니다. AMP 페이지를 작업하기에 앞서 선행되어야 할 사항으로 구글 페이지 인사이트에서 모바일 사용환경 테스트 점수를 80점 이상으로 통과한 상태여야 하며, 추가로 데이터 구조화 유형 중 아티클을 만족해야 합니다. 아티클 유형으로는 Article, NewsArticle, BlogPosting 또는 VideoObject입니다. 이 네 가지 유형에 속하지 않는 웹사이트는 AMP를 적용하는 것이 현재 무의미합니다. 그러면, 일반 게시판이나 위키, 쇼핑몰은 어디에 속할까요? 대부분 Article 유형에 속합니다.

Note: 선택적으로는 SSL을 지원하는 것이 좋습니다. <amp-form> 이나 <amp-video><amp-img>를 제외한 URL 속성을 필요로 하는 대부분의 AMP 컴포넌트가 SSL 주소를 요구하기 때문입니다.

여기부터 작성되는 내용은 위 두 조건을 모두 만족한 것을 전제로 합니다. 이제 AMP HTML을 작성해 봅시다. 기존 HTML에 AMP 모드를 추가할 것인지 아니면 AMP 페이지를 별도로 제공할 것인지를 우선 결정해야 합니다. 여기에는 장점과 단점이 존재합니다. 전자는 작업효율이 높습니다. 이미 잘 돌아가고 있는 HTML을 건드릴 필요가 없으니까요. 스펙 문서를 보고 마구잡이로 때려 고쳐 내려가면서 검사기를 통과시키면 됩니다. 단점은 관리 포인트가 늘어나는 것이죠. 후자는 고스란히 그 반대입니다. 굉장히 하드코어한 코딩을 구사하게 될 거에요. 저는 이 두 가지 방법을 모두 시도하는 것을 추천해 드립니다. 최초 페이지를 별도로 작업하다 보면 두 문서 간의 차이점을 쉽게 찾아낼 수 있고요. 결국에는 하나의 작업물에서 두 가지 아웃풋을 내는 형태로 병합할 수 있습니다.

자, 이제 AMP HTML 스펙 문서를 열고 코딩하세요. 코딩하면서 아래의 유의해야 할 항목을 살펴보세요. 이것은 제가 범한 실수이고 오류가 보고될 때마다 기록한 것입니다. 작게 여겨질 수도 있겠지만, 오류 하나가 보고되면 이 오류 항목이 사라지기까지는 보통 1달 정도 혹은 그 이상의 시간이 소요된다는 점 꼭 유념하시기 바랍니다.

  • AMP 구분자와 언어코드를 HTML에 삽입해 주세요. <html amp lang="ko">
  • UTF-8이 기본입니다. <meta charset="utf-8">
  • <link rel="canonical"> 태그의 값에는 원래 문서의 고유 주소를 사용하세요.
  • <link rel="canonical"> 주소에는 필터와 같은 파라미터가 기입되지 않도록 하여 하나의 문서가 여러 링크를 만들어 내는 일이 없도록 해야합니다.
  • m.firejune.com과 같이 모바일용 페이지를 별도로 제공하는 경우 <link rel="alternate">를 이용하세요.
  • <meta name="viewport" content="width=device-width ..."> 필수 요소 입니다.
  • <script async src="https://cdn.ampproject.org/v0.js"> 역시 필수 요소입니다.
  • 위 스크립트가 삽입되면 주소창의 마지막에 #development=1를 붙여 콘솔 로그를 통해 실시간으로 오류를 확인할 수 있습니다.
  • 페이지에서 필요로 하는 스타일시트를 모두 <style amp-custom> 요소에 인라인으로 삽입하세요. 단, 50k를 넘겨선 안 됩니다.
  • <style amp-custom>에서 이미 정의된 <amp-*> 요소의 스타일을 재정의 해선 안 됩니다.
  • <style amp-custom>에서 CSS의 값에 !important를 사용할 수 없습니다.
  • <head>에 선언된 <style amp-custom> 외 그 어떤 곳에도 <style> 태그는 허용되지 않습니다.
  • 일반 <script> 태그도 마찬가지로 절대 허용하지 않습니다.
  • 서드파티 라이브러리 포함하여 페이지에 삽입된 모든 자바스크립트를 삭제하세요.
  • 요소에 인라인으로 작성된 on*= 이벤트와 style= 속성을 모두 삭제하세요.
  • 별도의 확장 컴포넌트 없이 사용할 수 있는 <amp-img>, <amp-video>, <amp-audio>, <amp-pixel>, <amp-ad> AMP 태그를 치환하는 작업을 먼저 수행 하세요.
  • 모든 <amp-*> 요소에는 style=on*= 속성을 사용할 수 없습니다.
  • <amp-video> 요소의 src 속성은 절대 경로여야 하며 https 프로토콜을 이용해야 합니다.
  • 확장 컴포넌트(추가적인 로딩이 필요한)에 상응하는 AMP 요소로 치환하고 동적으로 컴포넌트를 로딩하는 루틴을 구현하세요.
  • 기존 <iframe>으로 삽입된 youtube나 vimeo 동영상은 각 상응하는 컴포넌트를 이용하여 삽입해야 합니다. <amp-iframe>으로만 치환하면 서비스 종류에 따라서 오류가 발생하기도 합니다.
  • <form>의 자식 요소로 사용되는 모든 요소(예: input)는 <form>안에서만 사용할 수 없습니다.
  • 대부분의 AMP 디스플레이 요소에 사용할 수 있는 layout 속성의 값으로 "responsive"를 이용하면 크기를 자동으로 계산해 줍니다.
  • AMP 디스플레이 요소의 필수 값인 heightwidth속성의 단위는 픽셀이어야 합니다.
  • <amp-sidebar> 요소는 <body> 바로 하위에 위치해야 합니다.
  • 확장 컴포넌트는 다양한 방법으로 응용할 수 있습니다.
  • 어느 정도 마무리되면 웹 기반 유효성 검사기를 돌려봅니다.
  • 끝으로 <link rel="amphtml"> 태그를 고유 문서의 <head>에 삽입하고 크롤링 당합니다.
  • <link rel="amphtml"> 태그의 값에는 원래 문서 주소의 규칙을 유지하는 것이 좋습니다.
  • 서로 다른 고유주소를 가진 문서가 하나의 <link rel="amphtml"> 주소를 가르키지 않도록 해야합니다. 오류가 두 달 이상 사라지지 않고 있네요.
Accelerated Mobile Pages.png
Google Search Console(firejune.com) > Search Appearance > Accelerated Mobile Pages

모든 유효성 검사를 통과하고 크롤러가 문서를 긁어가면 구글 AMP 캐시에 저장되고 그 결과를 위 그림처럼 서치 콘솔에서 확인할 수 있습니다. 그리고 "업데이트" 메시지가 나타나면 이후 부터 검색 결과에 노출되기 시작합니다. AMP 캐시 서버에 저장되는 대상은 HTML 문서와 포함된 이미지 파일이며 그외 정적인 파일은 SSL기반의 핫링크가 걸리게 되죠. 캐시된 문서는 다음과 같이 REST API를 이용하여 접근 하거나, 갱신됨을 알리(핑)거나, 캐시된 URL 목록을 호출하거나, 삭제요청을 직접적으로 수행할 수도 있습니다.

# Request for an AMP HTML document
GET https://cdn.ampproject.org/c/s/example.com/amp_document.html
# Request for an image
GET https://cdn.amproject.org/i/example.com/logo.png
# Request an AMP URL
POST https://acceleratedmobilepageurl.googleapis.com/v1/ampUrls:batchGet
# Update ping
GET https://cdn.ampproject.org/update-ping/c/s/ampbyexample.com
# Remove AMP content
GET https://cdn.ampproject.org/update-ping/i/s/example.com/favicon.ico

Note: 캐시 상태를 신속하게 동기화 할 수 있는 수단이며, 일반적으로 크롤러가 알아서 하기 때문에 선택사양입니다.

우와…. 이걸 다 언제 작업하나요…. 아…. 이 짓을 다시 한다는 것은 군대를 다시 들어가는 그런 느낌이군요. 그래서 조금이나마 도움을 드리고자 PHP로 작성한 Ampify 클래스를 공유합니다. 이 정적인 클래스는 GeSHi라는 라이브러리를 이용하여 코드의 문법 강조 기능을 포함하고 있습니다. 응답이 많이 느려지지만 클라이언트에서 할 수 없으니 백-엔드에서 할 수밖에요. 나중에 확장 컴포넌트로 나오려나요? 어차피 기계가 방문할 페이지인데 좀 느려도 괜찮겠죠. 그리고 빠른 이미지 크기 추출을 위해 fastimage.php를 필요로 합니다. 간단한 사용법은 Ampify::content($html) 메서드를 호출하여 일반 HTML을 AMP로 변환합니다. 더 자세한 내용은 코드에 있습니다. ;)

/**
 * Ampify class
 *
 * @license MIT
 * @author Firejune
 * @version 0.4.19
 */
class Ampify {
	// An var of list for source code syntax highlighting
	private static $highlights = array();
	// An var of list for extnend component loading
	private static $components = array();
	// Source code syntax highlight using GeSHi
	private static function codeHelper($matches) {
		require_once('lib/geshi/geshi.php');
		$lang = str_replace('language-', '', $matches[1]);
		$code = rtrim($matches[2]);
		if ($lang == 'json' || $lang == 'jsx') $lang = 'javascript';
		if ($lang == 'html' || $lang == 'markup') $lang = 'html5';
		if ($lang == 'command') $lang = 'bash';
		if (!in_array($lang, self::$highlights)) {
			self::$highlights[] = $lang;
		}
		$geshi = new GeSHi(str_tag_on(strip_tags($code)), $lang);
		$geshi->enable_classes();
		$code = $geshi->parse_code();
		$code = preg_replace('/<[ ]*pre( [^>]*)?>/i', '<pre$1><code>', $code);
		$code = preg_replace('/<\/pre>/i', '</code></pre>', $code);
		return self::fixClassName($code);
	}
	// Return defined styles of syntax highlight
	private static function getCodeStyle() {
		$geshi = new GeSHi;
		$languages = self::$highlights;
		foreach ($languages as $language) {
			$file = $geshi->language_path.$language.'.php';
			if (!file_exists($file)) {
				continue;
			}
			$geshi->set_language($language);
			$css .= preg_replace('/^\/\*\*.*?\*\//s', '', $geshi->get_stylesheet(false));
		}
		return $css;
	}
	// Fix class names of syntax type
	private static function fixClassName($code) {
		$entities =     array('class="command"', 'class="html"',  'class="json"',       'class="jsx"');
		$replacements = array('class="bash"',    'class="html5"', 'class="javascript"', 'class="javascript"');
		return str_replace($entities, $replacements, $code);
	}
	// Retrun Accepted HTML5 tags and AMP components
	private static function getAllowTags() {
		$html = '<h1><h2><h3><h4><h5><h6><a><p><ul><ol><li><blockquote><q><cite><ins><del><strong>'
			.'<em><code><pre><svg><table><thead><tbody><tfoot><th><tr><td><dl><dt><dd><article>'
			.'<section><header><footer><aside><figure><figcaption><time><abbr><div><span><hr><br>'
			.'<kbd><u><b><i><s><small><caption><address><button><source>';
		$amp = '<amp-embed><amp-img><amp-pixel><amp-video><amp-audio><amp-anim><amp-iframe><amp-fit-text>'
			.'<amp-access><amp-font><amp-slides><amp-ad><amp-list><amp-live-list><amp-social-share>'
			.'<amp-lightbox><amp-carousel><amp-accordion><amp-youtube><amp-vimeo>';
		return $html.$amp;
	}
	// Ignoring not allowed attribute of AMP for all tags.
	private static function ignoreHelper($m) {
		$tag = $m[0];
		$attributes = $m[2];
		preg_match_all('/([\w-]+)\s*(?:=\s*(?:"([^"]*)"|\'([^\']*)\'|(\w*[^\s>]*)))?/usix', $attributes, $matches);
		foreach ($matches[1] as $key => $val) {
			$attr = $matches[2][$key];
			if (!$attr) $attr = $matches[3][$key];
			if (!$attr) $attr = $matches[4][$key];
			if ($val == 'href' && strpos($attr, 'javascript:') !== false) {
				$tag = str_replace($attr, '#', $tag);
			}
			if (preg_match('/^(on(click|mousedown|mouseup|mousemove|mouseout|mouseover|load)|style|summary)/i', $val)) {
				$tag = str_replace($matches[0][$key], '', $tag);
			}
		}
		return $tag;
	}
	// An helper for image tag
	private static function imageHelper($m) {
		$attr = $m[1];
		$args = array();
		preg_match_all('/([\w-]+)\s*(?:=\s*(?:"([^"]*)"|\'([^\']*)\'|(\w*[^\s>]*)))?/usix', $attr, $matches);
		# Allowed attr of <amp-img>
		foreach ($matches[1] as $key => $val) {
			if (preg_match('/(src|srcset|layout|heights?|alt|role|on|tabindex|placeholder|widths?|data-*|type|class)/i', $val)) {
				$attr = $matches[2][$key];
				if (!$attr) $attr = $matches[3][$key];
				if (!$attr) $attr = $matches[4][$key];
				$args[$val] = $attr;
			}
		}
		if (!$args['layout']) {
			$args['layout'] = 'responsive';
		}
		if (strpos($args['width'], '%') !== false) {
			$args['width'] = 0;
		}
		if (strpos($args['height'], '%') !== false) {
			$args['height'] = 0;
		}
		if (!$args['width'] || !$args['height']) {
			require_once('lib/fastimage.php');
			$src = $args['src'];
			if (preg_match('/\/\/(m\.)?firejune(\.cafe24)?.com/i', $src)) {
				$src = preg_replace('/https?:\/\/(m\.)?firejune(\.cafe24)?.com/i', '', $src);
			}
			$src = preg_replace('/^\.\.?\//', '/', $src);
			if (!preg_match('/(https?:)?\/\/[^\/]+\//i', $src) && preg_match('/^\/images\//i', $src)) {
				$src = '/public'.$src;
			}
			if (strpos($src, '/public/images/') === 0) {
				$args['layout'] = 'fixed';
			}
			$img = new FastImage('./'.$src);
			$size = $img->getSize();
			if ($args['width']) {
				$args['height'] = intval($size[1] / $size[0] * $args['width'], 10);
			} elseif($args['height']) {
				$args['width'] = intval($size[0] / $size[1] * $args['height'], 10);
			} else {
				$args['width'] = $size[0];
				$args['height'] = $size[1];
			}
		}
		if ($args['width'] < 240 && $args['height'] < 240) {
			$args['layout'] = 'fixed';
		}
		if (!$args['src'] || !$args['width'] || !$args['height']) {
			return '';
		}
		$attr = '';
		foreach ($args as $key => $val) {
			$attr .= ' '.$key.'="'.$val.'"';
		}
		return '<amp-img'.$attr.'></amp-img>';
	}
	// An helper for video tag
	private static function videoHelper($m) {
		$tag = $m[0];
		$src = $m[1];
		$tag = preg_replace('/<amp-video(.*?)>/', '<amp-video$1 layout="responsive">', $tag);
		return str_replace($src, strip_tags($src, '<source>'), $tag);
	}
	// An helper for iframe tag
	private static function iframeHelper($m) {
		$tag = $m[0];
		$src = $m[1];
		# amp-youtube
		if (preg_match("/^(?:http(?:s)?:)?\/\/(?:www\.)?(?:m\.)?(?:youtu\.be\/|youtube\.com\/(?:(?:watch)?\?(?:.*&)?v(?:i)?=|(?:embed|v|vi|user)\/))([^\?&\"'>]+)/", $src, $id)) {
			preg_match('/<iframe.*width="([^"]\d+)".*height="([^"]\d+)".*>/', $tag, $val);
			return '<amp-youtube data-videoid="'.$id[1].'" layout="responsive" width="'.$val[1].'" height="'.$val[2].'"></amp-youtube>';
		}
		return $tag;
	}
	// Add using AMP extend compoenent to list
	public static function add($component) {
		if (is_array($component)) {
			$component = 'amp-'.$component[1];
		}
		if (!in_array($component, self::$components)) {
			self::$components[] = $component;
		}
	}
	// Convert contents to AMP compoents
	public static function content($html) {
  	$html = self::html($html);
		# Replace iframe tag to AMP component
		$html = preg_replace_callback('#<iframe.*?src="([^"]*)".*?[^>]*></iframe>#is', 'ampify::iframeHelper', $html);
		$html = preg_replace('/<amp-iframe(.*?)>/', '<amp-iframe$1 sandbox="allow-scripts" layout="responsive">', $html);
		# Reduce ignore attributes
		$html = preg_replace_callback('/<([\w-]+) ([^>]*)?>/si', 'ampify::ignoreHelper', $html);
		# Code syntax hightlight
		$html = preg_replace_callback('#<pre.*?class="([^"]*)".*?[^>]*><code>(.*?)?</code></pre>#is', 'ampify::codeHelper', $html);
		# Remove unnecessary tags
		$html = preg_replace('/<(script|style).*?<\/\1>/s', '', $html);
		# Whitelist of HTML tags allowed by AMP
		$html = strip_tags($html, self::getAllowTags());
		# Dynamic load amp components
		preg_replace_callback('/<\/amp-(youtube|iframe|accordion|carousel)>/', 'ampify::add', $html);
		return $html;
	}
	// Convert generic HTML to AMP
	public static function html($html) {
		# Replace img, audio, and video elements with amp custom elements
		$html = str_ireplace(
			array('<img', '<video', '/video>', '<audio', '/audio>'),
			array('<amp-img', '<amp-video', '/amp-video>', '<amp-audio', '/amp-audio>'),
			$html
		);
		# Add amp attribute to html tag
		$html = preg_replace("/<[ ]*html( [^>]*)?>/i", '<html amp$1> ', $html);
		# Fix display amp custom elements
		$html = preg_replace_callback('/<amp-img(.*?)\/?>/', 'ampify::imageHelper', $html);
		$html = preg_replace_callback('#<amp-video.*?[^>]*>(.*?)?</amp-video>#is', 'ampify::videoHelper', $html);
		return $html;
	}
	// Return stylesheets string from files
	public static function css($path, $name) {
		$css = file_get_contents($path.$name.'/css/master.css');
		$css .= file_get_contents($path.$name.'/css/article.css');
		$css .= file_get_contents($path.$name.'/css/responsive.css');
		if (!empty(self::$highlights)) {
			$css .= self::getCodeStyle();
		}
		return $css;
	}
	// Return extended components when it using
	public static function js() {
		foreach (self::$components as $component) {
			$js .= '<script async custom-element="'.$component.'" src="https://cdn.ampproject.org/v0/'.$component.'-0.1.js"></script>';
		}
		return $js;
	}
}

Note: 이곳의 사정에 맞게 코딩된 것으로, 모든 곳에서 작동한다는 보장은 못합니다. 참고용으로만 이용해 주세요.. :(

끝으로, 반가운 소식입니다. AMP 프로젝트 페이지의 한글화 작업이 진행 중이군요! 필독을 권장합니다. 그럼, 화이팅!

Comments

스마트폰에서 구글 검색을 하던 중 수년간을 버리다시피 하던 이곳의 컨텐츠가 검색 결과에 노출되었습니다. 그런데, “페이지가 모바일에 적합하지 않습니다.”라는 메시지가 뜨더군요? 이 문구는 모바일 사용환경에서 구글 웹 검색을 이용하는 경우 사용자에게 경고성으로 알리는 내용 같았습니다. 뭔가, 기분이 썩 좋지는 않습니다만, 신기하기도 하죠. 지가 뭔데 그런 심판(?)을 내리는지 말예요. 그동안 구글 검색이 무슨 짓거리를 했는지에 대하여 대략 살펴보았습니다.

2014년부터 구글 검색 크롤러가 더욱 똑똑해져서 페이지에 포함된 스타일시트와 자바스크립트가 컨텐츠에 영향을 끼치는 여부를 인지합니다. 다시 말해, 자바스크립트에 의해 생성된 내용도 검색 결과에 노출될 수 있으며, CSS에 의해 숨겨놓은 컨텐츠는 검색 대상에서 제외하는 등의 장치를 마련한 것입니다. 구글 서치 콘솔(구글 웹마스터 도구)에서 Fetch As Google에 “가져오기 및 렌더링”을 요청하면 “Googlebot에게 페이지가 다음과 같이 보입니다.” 라면서 마치 눈이라도 달라준 것 마냥 렌더링된 페이지를 보여주네요. 이와 동시에 2009년에 추가된 AJAX crawling scheme는 Deprecated 되었습니다.

2015년에는 모바일 환경에서 웹사이트를 이용하는 것이 병맛이라며 캠페인을 하더니, 극기야 로봇(크롤러)이 구림과 구리지 않음을 스스로 판단하기에 이릅니다. 그리고 하는 말이 "모바일에 친화적이지 않으면 모바일 검색 결과에 안 좋은 영향을 미치게 될 것"이라면서 테스트 도구를 하나 던져주고 시험에 통과할 것을 강요합니다.

mobile-usability.png
Google Search Console(firejune.com) > Search Traffic > Mobile Usability

노트: 모바일 친화성 테스트에 통과하려면 구글페이지 스피드 인사이트 모바일 섹션에서 제공하는 가이드라인에 맞게 사이트를 수정하고, 사용자 환경 점수를 80점 이상 받아야 통과되며, 디바이스 기준은 안드로이드 6.0.1 넥서스5X(Build/MMB29P)입니다. 수정 후 반영까지 규모에 따라 다르겠지만, 최소 한 주 정도 소요되는 것으로 보입니다.

그리고 2016년에는 구글 AMP(Accelerated Mobile Pages)라는 프로젝트를 서치 콘솔에 집어넣고 자기들 검색 결과에 색다른 출력 방법을 꾀하려고 합니다. 이 프로젝트는 이름에서 알 수 있듯이 모바일에서 웹 컨텐츠를 더욱 빠르고 쉽게 접근할 수 있도록 하자는 목표를 가진 구글에서 추진 중인 프로젝트입니다. 자세한 내용을 유스풀패러다임에서 잘 정리해 주셨네요.

제가 이해한 바로는, "당신들이 만든 사이트는 하나같이 전부 거지 같으니, 우리가 제시하는 지침대로 다시 만들어서 제출하면 사용자들은 엄청나게 빠르고 편리한 모바일 사용경험을 누리게 될 거고, 당신은 트래픽 비용을 절감할 수 있을 거예요." 여기서 빠르고 편리하다는 의미는 사용자가 검색 결과를 클릭했을 때 사용자의 사이트로 직접 연결하지 않고 AMP 캐시로부터 컨텐츠를 불러와 검색 사이트 내에서 보여주는 것을 말하는 것입니다. 아웃바운드 링크를 인바운드 링크로 둔갑시켜 사용자가 구글 사이트를 떠나지 못하게 잡아두겠다는 의미로도 해석되지요. 또한, AMP 캐시에 저장된 데이터는 아무런 조건 없이 누구나 사용할 수 있게 될 것이랍니다. 즉 페이스북이나 트위터와 같은 곳에서도 같은 목적으로 사용할 수 있게 된다는 뜻이고요.

AMP 지침을 살짝 들여다보면 모바일 친화적인 건 기본이고 AMP에서 제시하는 태그를 사용할 것, 허용하지 않는 속성은 사용하지 말 것, 사용자가 작성한 자바스크립트 쓰지 말 것, 스타일시트는 적정량의 인라인으로 넣을 것 등 수십 가지 유효성 규칙이 존재하며 검사를 통과해야 합니다.

accelerated-mobile-pages.png
Google Search Console(firejune.com) > Search Appearance > Accelerated Mobile Pages

도대체 얼마나 빨라진다는 건지 궁금하기도 하고, 대략 이 정도면 서로 도움되는 그림인 것 같아 그냥 적용해 보기로 한 것입니다. 그래프에서 알 수 있듯이 작업을 시작한 시점이 약 두 달 전입니다. 당시 아래와 같은 백로그를 작성하고 짬짬이 수행했고요. 지금은 모두 완료된 상태입니다. 원래 AMP를 적용하는 과정에 대한 기술적 내용을 다루려고 했으나 뻘소리만 늘어놓는 바람에 프롤로그만 작성했습니다. 다음번에 별도로 AMP 적용기를 작성하도록 하겠습니다.

  • 오류가 발생하는 구조화된 데이터 모두 수정
  • 구조화된 데이터에 Breadcrumbs, Searchbox 추가
  • 구글 서치 콘솔의 모바일 사용 편의성에 보고된 오류 모두 수정
  • 액셀러레이티드 모바일 페이지 지원
  • 모바일 사이트 별도 제공(Cafe24에서 제공하는 기능은 Path를 잘라 먹음)
  • Cafe24에서 UDP소켓 막아버림, Node서버를 이용한 Twitter 연동
  • 로그인 페이지 개구림
  • PDF 모바일에서 깨짐
  • 서버-사이드 문법강조기 추가
  • SSL 적용(미친 Cafe24 SSL 포트를 48408 이렇게 주면 어떻게 쓰라는 거야?)
  • HTTP/2 + SPDY 적용
  • 당분간 Non SSL을 위한 CORS 구현
  • PQP 프로파일러 붙여서 SQL 및 코드 성능개선

Comments

Aria Fallah씨는 Webpack을 시작하기가 쉽지만은 않았다고 합니다. 그래서 친절하고 개괄적인 초보자용 Webpack 입문서를 만들었습니다. 그는 이 튜토리얼을 통해 Webpack의 사용법을 쉽게 배울 수 있기를 바란다고 했습니다.

1-1. Webpack을 왜 사용하나요?

여기에 Webpack을 사용해야 할 몇 가지 현실적인 이유가 있습니다.

  • 하나의 파일로 js 파일을 번들할 수 있습니다.
  • 프론트엔드 코드에 npm 패키지를 사용할 수 있습니다.
  • ES6/ES7 자바스크립트 코드를 작성할 수 있습니다. (Babel을 이용하여)
  • 코드를 압축 또는 최적화할 수 있습니다.
  • LESS/SCSS를 CSS로 돌릴 수 있습니다.
  • HMR(Hot Module Replacement)을 사용할 수 있습니다.
  • 자바스크립트로 모든 유형의 파일을 포함할 수 있습니다.
  • 이 글에서 다루지 못한 아주 많은 고급기능이 있습니다.
왜 이러한 기능이 필요한가요?
  • js 파일 번들 - 자바스크립트를 모듈로 작성할 수 있습니다, 그래서 각각의 파일에 대해서 <script> 태그를 별도로 작성할 필요가 없습니다. (상황에 따라서 둘 이상의 js 파일이 필요한 경우 구성 가능함)
  • npm 패키지 사용 - npm은 인터넷상에서 오픈소스 코드의 커다란 생태계입니다. npm 코드를 저장할 기회가 주어지며, 원하는 프론트엔드 패키지를 가져다 쓸 수 있습니다.
  • ES6/ES7 - 많은 기능을 추가되어 더 강력하고 더 쉽게 자바스크립트를 작성할 수 있습니다. 여기에 소개하는 글이 있습니다.
  • 코드 압축/최적화 - 배포되는 파일의 크기를 줄입니다. 페이지 로딩이 빨라지는 등의 장점을 포함합니다.
  • LESS/SCSS를 CSS로 돌리기 - CSS를 작성하는 더 좋은 방법입니다. 여기에 소개하는 글이 있습니다.
  • HMR 사용 - 생산성이 향상됩니다. 코드를 저장할 때 마다 페이지의 리프레시가 자동으로 이루어집니다. 코드를 작성하는 동안 페이지의 상태를 최신으로 유지해야 하는 경우 정말 편리합니다.
  • 자바스크립트로 모든 유형의 파일을 포함 - 추가적인 빌드 도구의 수를 줄일 수 있고, 프로그램적으로 파일을 사용 및 수정할 수 있습니다.

1-2. 기본 익히기

1-2-1. 설치하기

Webpack의 모든 기능을 사용하려면 전역으로 설치해야 합니다:

npm install -g webpack

그러나 Webpack의 일부 기능이나 최적화 플러그인 정도만 필요한 경우라면 로컬에 설치합니다:

npm install --save-dev webpack
실행 명령

Webpack을 실행하려면:

webpack

Webpack에서 파일의 상태가 변경되면 자동으로 빌드하려는 경우:

webpack --watch

특정한 이름의 사용자가 정의한 Webpack 설정 파일을 사용하려면:

webpack --config myconfig.js

1-2-2. 번들하기

예제 1

Official Dependency Tree

Webpack은 공식적으로 모듈 번들러라고 합니다. 다음의 두 가지 훌륭한 글은 모듈 액세스에 대한 깊이 있는 설명과 명확한 모듈 번들링에 대하여 다루고 있습니다: 이것이것.

간단하게 봅시다. 작동시키는 방법은 하나의 파일을 진입점으로 지정하는 것입니다. 이 파일은 트리의 루트가 될 것입니다. 그러면 require에 의해 다른 파일이 트리에 추가됩니다. webpack 명령을 실행하면, 모든 파일과 모듈은 하나의 파일에 번들로 제공됩니다.

다음은 간단한 예제입니다:

Dependency Tree

이 그림은 다음과 같은 디렉터리 구조를 가진다고 가정합니다:

MyDirectory
|- index.js
|- UIStuff.js
|- APIStuff.js
|- styles.css
|- extraFile.js

이것은 파일의 내용입니다.

// index.js
require('./styles.css')
require('./UIStuff.js')
require('./APIStuff.js')

// UIStuff.js
var React = require('React')
React.createClass({
  // stuff
})

// APIStuff.js
var fetch = require('fetch') // fetch polyfill
fetch('https://google.com')
/* styles.css */
body {
  background-color: rgb(200, 56, 97);
}

webpack 명령을 실행하면, 이 트리의 내용을 번들로 얻을 수 있겠지만, 같은 디렉터리에 있는 extraFile.jsrequire에 참조되지 않았기 때문에 결코 번들의 일부가 되지 않습니다.

bundle.js는 다음과 같이 표시됩니다:

// contents of styles.css
// contents of UIStuff.js + React
// contents of APIStuff.js + fetch

즉, 번들로 제공되는 것들은 파일의 참조를 통한 경우의 것들입니다.

1-2-3. 로더란?

이미 눈치챘겠지만, 위의 예제에서 이상한 일을 저질렀습니다. 바로 CSS 파일을 자바스크립트 파일에 require를 사용한 것입니다. 이것은 정말 멋집니다, Webpack의 흥미로운 점은 require에 자바스크립트 파일 말고도 다른 것을 더 할 수 있다는 것입니다.

Webpack에는 로더라는 것이 있습니다. 이 로더를 사용하면, require를 이용하여 .css.html, .png 등을 불러올 수 있습니다.

위 그림의 예를 들어 보겠습니다.

// index.js
require('./styles.css')

Webpack의 구성에 스타일-로더CSS-로더를 포함하는 경우, 이것은 단지 완전히 유효하지만 않을 뿐, 실제로는 페이지에 CSS를 적용하게 됩니다.

이것은 Webpack과 함께 사용할 수 있는 수많은 로더들 중 하나의 사용 예제일 뿐입니다.

1-2-4. 플러그인

플러그인은 이름에서 알 수 있듯이, Webpack에 사용할 수 있는 추가 기능입니다. 자주 사용하는 플러그인 중 하나는 UglifyJsPlugin입니다. 이는 자바스크립트 코드를 압축(minify)해 줍니다. 이 사용법에 대해서는 나중에 다룰 것입니다.

1-3. 설정 파일 구성

Webpack은 박스(?) 밖에서 작동하지 않기 때문에 필요에 맞게 작성해야 합니다. 이를 위해 다음과 같은 파일을 생성할 수 있습니다.

webpack.config.js

이것은 Webpack이 기본적으로 인식하는 파일명입니다. 다른 이름을 사용하려면 해당 파일의 이름을 지정할 수 있는 --config 플래그를 사용해야 합니다.

1-3-1. 최소한의 예제

예제 2

디렉터리 구조는 다음과 같습니다:

MyDirectory
|- dist
|- src
   |- index.js
|- webpack.config.js

다음으로 최소한의 Webpack 설정이 있습니다.

// webpack.config.js
var path = require('path')

module.exports = {
  entry: ['./src/index'], // file extension after index is optional for .js files
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  }
}

새롭게 보이는 속성을 각각 살펴봅시다:

  • entry - 번들의 엔트리 포인트로써 번들하기 색션에서 이미 논의했습니다. Webpack은 여러 번들을 생성하는 진입점을 허용하기 때문에 배열입니다.
  • output - Webpack의 최종 결과물이 되는 형태를 명시합니다.
    • path - 어디에 번들 파일을 위치시킬 것인지를 지정합니다.
    • filename - 번들 파일의 이름을 지정합니다.

이제 webpack 명령을 실행하면, dist라는 폴더에 bundle.js 파일을 생성합니다.

1-3-2. 플러그인 이해

예제 3

모든 파일의 번들에 Webpack을 사용했고 모두 합쳐서 900KB 짜리 파일을 얻었다고 가정해 봅시다. 덩치가 큰 문제는 번들 파일의 압축으로 개선될 수 있습니다. 이 작업을 수행하려면 앞서 언급했던 UglifyJsPlugin라는 플러그인을 사용합니다.

또한 실제로 플러그인을 사용할 수 있도록 Webpack을 로컬에 설치해야 합니다.

npm install --save-dev webpack

이제 Webpack에서 필요로 하는 코드를 압축할 수 있습니다.

// webpack.config.js
var path = require('path')
var webpack = require('webpack')

module.exports = {
  entry: ['./src/index'],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },

  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
      },
    })
  ]
}

새롭게 보이는 속성을 각각 살펴봅시다:

  • plugins - 보유 중인 플러그인의 배열입니다.

이제, webpack 명령을 실행하면, UglifyJsPlugin에 의해 모든 공백을 제거하는 등의 과정을 거쳐 900KB 짜리 파일을 200KB로 줄일 수 있습니다.

또한 OccurrenceOrderPlugin을 추가할 수도 있습니다.

이 플러그인은 발생 횟수에 따라서 모듈 및 청크 id를 할당합니다. 자주 사용되는 id가 낮은(짧은) id를 얻습니다. 이 id는 예측(predictable)이 가능하며, 전체 파일 크기를 줄이는데(역자주: 파일 용량을 줄이는 것과는 무관함) 추천됩니다.

솔직히 말해서 기반 메커니즘이 어떻게 작동하는지 잘 모르지만, Webpack2 베타 버전에는 기본으로 포함 되어 있다고 합니다.

// webpack.config.js
var path = require('path')
var webpack = require('webpack')

module.exports = {
  entry: ['./src/index'],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
      },
    }),
    new webpack.optimize.OccurenceOrderPlugin()
  ]
}

여기까지 자바스크립트의 번들을 압축하는 설정을 작성했습니다. 이 번들을 다른 프로젝트의 디렉터리에 붙여넣고 <script> 태그에 대입할 수 있습니다. 여기에서 결론으로 바로 넘어가도 좋습니다. 오직 자바스크립트 에 대한 기본적인 Webpack 사용법만 필요하다면 말이죠.

1-4. 조금 더 복잡한 예제

추가적으로, Webpack은 자바스크립트에 관련한 단순 작업보다 더 많은 일을 할 수 있으므로, 수동으로 복사-붙여넣기 하는 일을 없애고 Webpack으로 전체 프로젝트를 관리할 수 있습니다.

다음 섹션에서는, Webpack을 사용하여 아주 간단한 웹사이트를 만들 것입니다. 예제를 수행하고자 하는 경우, 다음과 같은 구조의 디렉터리를 생성하세요.

MyDirectory
|- dist
|- src
   |- index.js
   |- index.html
   |- styles.css
|- package.json
|- webpack.config.js
학습 내용
  1. 로더 이해하기 - 번들에 CSS를 추가할 수 있도록 로더를 추가해 볼 것입니다.
  2. 플러그인 추가하기 - HTML 파일을 생성하고 사용할 수 있도록 도와주는 플러그인을 추가해 볼 것입니다.
  3. 개발서버 구성하기 - developmentproduction을 구분한 Webpack의 구성 파일을 분할하고 webpack-dev-server를 이용하여 HMR을 활성화해 볼 것입니다.
  4. 코딩 시작하기 - 실제로 자바스크립트의 일부를 작성해 볼 것입니다.

1-4-1. 로더 이해하기

예제 4

이전 튜토리얼에서 로더에 대해 언급했습니다. 이제 자바스크립트가 아닌 파일을 다루어 보기로 하겠습니다. 스타일 로더와 CSS 로더가 필요하게 되었습니다. 먼저 로더를 설치해 봅시다:

npm install --save-dev style-loader css-loader

설치된 CSS 로더를 포함하도록 설정 파일을 조정해 봅시다:

// webpack.config.js
var path = require('path')
var webpack = require('webpack')

module.exports = {
  entry: ['./src/index'],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
      },
    }),
    new webpack.optimize.OccurenceOrderPlugin()
  ],
  module: {
    loaders: [{
      test: /\.css$/,
      loaders: ['style', 'css']
    }]
  }
}

새롭게 보이는 속성을 각각 살펴봅시다:

  • module - 이 옵션은 파일에 영향을 줍니다.
    • loaders - 애플리케이션에서 사용할 로더를 배열로 지정합니다.
      • test - 정규식을 이용해서 로더에 사용될 파일을 검출합니다.
      • loaders - 일치하는 파일에 사용되는 로더를 호출합니다.

webpack 명령을 실행하면, .css로 확장자를 가진 파일을 require하는 경우, 이 파일은 stylecss 로더에 적용되고, 번들에 CSS가 추가됩니다.

로더를 가지고 있지 않은 경우, 다음과 같은 오류를 보게 될 것입니다:

ERROR in ./test.css
Module parse failed: /Users/Developer/workspace/tutorials/webpack/part1/example1/test.css
Line 1: Unexpected token {
You may need an appropriate loader to handle this file type.

선택사항

만약 CSS 대신 SCSS를 사용하는 경우 다음과 같이 실행해야 합니다:

npm install --save-dev sass-loader node-sass webpack

그리고 로더는 다음과 같이 작성되어야 합니다.

{
  test: /\.scss$/,
  loaders: ["style", "css", "sass"]
}

이 과정은 LESS도 비슷합니다.

중요한 것은 지정할 순서가 존재한다는 것입니다. 위의 예제에서 sass 로더에 가장 먼저 .scss 파일을 적용하고, 그다음으로 css 로더, 마지막에 style 로더에 적용합니다. 즉, 순서 패턴은 오른쪽에서 왼쪽으로 로더에 적용되는 것입니다.

1-4-2. 플러그인 추가하기

예제 5

이제 웹사이트의 스타일링을 위한 인프라를 구축했으니, 스타일을 적용할 실제 페이지가 필요하게 되었습니다. HTML 페이지를 생성하거나 기존의 것을 그대로 사용할 수 있는 html-webpack-plugin을 이용하여 이 작업을 수행할 수 있습니다. 여기서는 기존의 index.html를 사용할 것입니다.

먼저 플러그인을 설치합니다:

npm install --save-dev [email protected]

다음으로 설정을 추가 합니다.

// webpack.config.js
var path = require('path')
var webpack = require('webpack')
var HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: ['./src/index'],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
      },
    }),
    new webpack.optimize.OccurenceOrderPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ],
  module: {
    loaders: [{
      test: /\.css$/,
      loaders: ['style', 'css']
    }]
  }
}

이번에 webpack 명령을 실행하면, HtmlWebpackPlugin./src/index.html 지정하기 때문에 ./src/index.html 파일의 내용을 dist 폴더에 index.html파일로 생성할 것입니다.

index.html의 내용이 비어 있다면 기본 템플릿을 사용하기 때문에 아무런 문제가 없습니다. 하지만 그 내용을 채워 보도록 하겠습니다.

<html>
<head>
  <title>Webpack Tutorial</title>
</head>
<body>
  <h1>Very Website</h1>
  <section id="color"></section>
  <button id="button">Such Button</button>
</body>
</html>

이제 bundle.js의 내용을 HTML의 <script> 태그에 직접 넣지 않아도 됩니다. 이 플러그인이 자동으로 넣어주기 때문입니다. 만약 스크립트 태그에 직접 넣은 경우라면, 같은 코드를 두 번 로드하게 될 것입니다.

이 시점에서 styles.css에 몇 가지 기본 스타일을 추가해 봅시다.

h1 {
  color: rgb(114, 191, 190);
  text-align: center;
}

#color {
  width: 300px;
  height: 300px;
  margin: 0 auto;
}

button {
  cursor: pointer;
  display: block;
  width: 100px;
  outline: 0;
  border: 0;
  margin: 20px auto;
}

1-4-3. 개발서버 구성하기

예제 6

이제 실제로 브라우저에서 웹사이트를 볼 수 있도록, 지금까지 만들어진 코드를 제공하는 웹서버가 필요하게 되었습니다. 편리하게도, Webpack은 webpack-dev-server를 제공합니다. 로컬과 글로벌에 모두 설치합니다.

npm install -g webpack-dev-server
npm install --save-dev webpack-dev-server

개발 서버는 작업 된 웹사이트를 브라우저에서 바로 확인할 수 있어 매우 유용하며, 더 빠른 개발을 할 수 있습니다. 기본적으로 http://localhost:8080를 방문할 수 있습니다. 아쉽지만, 핫-리로드 기능은 박스(?) 밖에서는 작동하지 않아서 약간의 추가 구성이 필요합니다.

이 시점에서 개발용(development)과 제품용(production)을 구분해 보겠습니다. 이 튜토리얼은 간단함을 유지하고 있으므로 큰 차이는 없지만, Webpack의 단적인 기능 설정에 관한 예입니다. webpack.config.dev.jswebpack.config.prod.js를 호출할 수 있도록 합니다.

// webpack.config.dev.js
var path = require('path')
var webpack = require('webpack')
var HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  devtool: 'cheap-eval-source-map',
  entry: [
    'webpack-dev-server/client?http://localhost:8080',
    'webpack/hot/dev-server',
    './src/index'
  ],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ],
  module: {
    loaders: [{
      test: /\.css$/,
      loaders: ['style', 'css']
    }]
  },
  devServer: {
    contentBase: './dist',
    hot: true
  }
}

바뀐점

  1. 개발 설정에서 지속해서 다시 구축하거나 최적화하는 일은 불필요한 오버헤드가 발생하기 때문에 생략합니다. 그래서 webpack.optimize 플러그인이 없습니다.
  2. 개발 설정은 개발 서버에 필요한 것만 작성합니다. 더 자세한 내용을 여기에서 볼 수 있습니다.

요약:

  • entry: 두 개의 새로운 엔트리 포인트는 HMR이 가능하도록 브라우저에 서버를 연결합니다.
  • devServer
    • contentBase: 브라우저에서 접근하는 파일의 위치입니다.
    • hot: HMR 사용 여부입니다.

제품용 설정의 구성은 별로 변경되지 않습니다.

// webpack.config.prod.js
var path = require('path')
var webpack = require('webpack')
var HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  devtool: 'source-map',
  entry: ['./src/index'],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
      },
    }),
    new webpack.optimize.OccurenceOrderPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ],
  module: {
    loaders: [{
      test: /\.css$/,
      loaders: ['style', 'css']
    }]
  }
}

또한, 개발용 구성과 제품용 구성 모두 새로운 속성을 추가했습니다:

  • devtool - 디버깅을 지원합니다. 오류가 발생하는 경우, 크롬 개발자 콘솔과 같은 도구를 이용하여 실수한 위치를 확인하는 데 도움됩니다. source-mapcheap-eval-source-map의 차이에 대해서는 문서를 읽고도 이해하기가 조금 어려웠지만, 확실히 알 수 있었던 것은 source-map이 제품용 모드에서 오버헤드가 많다는 점과, cheap-eval-source-map이 더 작은 오버헤드를 가지며, 이것은 단지 개발을 위한 것이라는 점입니다.

개발 서버는 다음과 같이 실행합니다.

webpack-dev-server --config webpack.config.dev.js

제품용 코드를 구축하기 위해서는 다음과 같이 실행합니다.

webpack --config webpack.config.prod.js

이와 같은 명령을 매번 입력하지 않아도 되도록 package.json에 약간의 기능을 작성하는 것으로 더 간단하게 명령을 수행할 수 있습니다.

설정에 scripts 속성을 추가합니다.

// package.json
{
  //...
  "scripts": {
    "build": "webpack --config webpack.config.prod.js",
    "dev"  : "webpack-dev-server --config webpack.config.dev.js"
  }
  //...
}

이제 더욱 간단하게 명령을 실행할 수 있습니다.

npm run build
npm run dev

npm run dev 명령을 실행하고 http://localhost:8080으로 이동하여 작업 된 웹사이트를 볼 수 있습니다.

노트: 이 부분을 테스트하는 동안 index.html 파일을 수정할 때 서버가 핫-리로드 되지 않는 것을 깨달았습니다. 이 문제에 대한 해결책은 html-reload에 있습니다. 이것은 Webpack 옵션에 대하여 조금 더 유용한 정보를 얻을 수 있어서 읽어보길 추천합니다. 너무 사소한 내용이고 튜토리얼을 너무 길게 쓰는 느낌이 들었기 때문에 별도로 구분했습니다.

1-4-4. 코딩 시작하기

예제 7

많은 사람이 Webpack에 당황해할 것 같습니다. 이유는 실제 작업용 자바스크립트 코드를 작성하기까지 여태것 공부했던 여러 과정을 모두 숙지해야 하기 때문입니다; 다행히도 이 튜토리얼의 클라이막스에 도달했습니다.

npm run dev 명령 실행 및 http://localhost:8080를 하지 않았으면 수행합니다. 개발 서버에 핫-리로드를 설정하는 것은 단순히 보기만을 위한 것이 아닙니다. 프로젝트 일부를 편집하고 저장하는 매시간 변경사항을 표시하도록 브라우저는 다시 로드합니다.

이제 이것을 프론트엔드에서 사용할 수 있는 방법을 보여주기 위해 몇몇 npm 패키지가 필요합니다.

npm install --save pleasejs

PleaseJS는 임의 색상 발생기입니다. 특정 버튼을 이용해 div의 색상을 변경해 보겠습니다.

// index.js

// Accept hot module reloading
if (module.hot) {
  module.hot.accept()
}

require('./styles.css') // The page is now styled
var Please = require('pleasejs')
var div = document.getElementById('color')
var button = document.getElementById('button')

function changeColor() {
  div.style.backgroundColor = Please.make_color()
}

button.addEventListener('click', changeColor)

흥미롭게도, HMR이 작동하려면 다음과 같은 코드를 포함해야 합니다:

if (module.hot) {
  module.hot.accept()
}

모듈 또는 상위 모듈에서요.

이제 마지막입니다!

노트: 예제를 통해 CSS를 적용하면서 꺼림칙한 부분은, CSS가 자바스크립트 파일에 있다는 사실입니다. 다른 파일에 CSS를 넣는 방법에 대한 자세한 설명을 별도로 작성했습니다. css-extract를 확인하세요.

1-5. 결론

축하합니다! div의 색상을 변경하는 버튼을 만들기까지 모두 학습했습니다! Webpack, 참 훌륭하죠?

Webpack은 처음 접한 모듈 번들러입니다. 그리고 매우 유용한 도구였습니다. 파트 1에서는 가장 일반적인 사용 사례를 다루었지만, 아직 ES6과 React를 연결하여 사용하는 방법에 대해서는 다루지 않았습니다.

나중에

  • 파트 2에서는 Webpack과 Babel을 함께 사용하여 ES6을 ES5로 transpile 하는 방법을 살펴보겠습니다.
  • 파트 3에서는 Webpack과 React + Babel을 함께 사용하는 방법을 살펴보겠습니다.

도움이 되었기를 바랍니다.

Comments