Web Assets – Tips for Better Organization and Performance
과거에는 프로젝트 리소스(이미지, css 등) 을 최소화하기 위해 엄청난 시간을 들였습니다. 이제는 인터넷이 빨라져서 훨씬 큰 플래쉬 파일이나 동영상, 이미지들을 사용할 수 있게 되었습니다. 하지만 모바일이 중요해지면서, 다시 과거로 돌아간 모양새 입니다. 최적화를 잘해서 반응속도를 높이는 일이 중요해 졌습니다.
Images
적당한 사이즈를 사용하기
종종 한 파일을 웹사이트 여러군데서 사용합니다. 예를 들어 쇼핑몰에서 모든 제품들은 이미지가 있습니다. 각 제품의 이미지를 보여주는 페이지가, 제품 리스트 페이지, 제품의 상세 설명 페이지, 또는 제품의 확대 이미지를 보는 페이지, 3개의 페이지가 있다고 해봅시다.
만약 이 세 페이지에서 같은 이미지 파일을 사용한다면, 브라우저는 제품 리스트 페이지에서도 아주 큰 파일을 다운 받아야 할 겁니다. 만약 원본 이미지가 1MB 정도이고 페이지당 10개의 제품을 보여준다면, 유저는 10MB 의 이미지를 다운받아야 합니다. 좋지 않죠. 가능하면 필요에 따라 여러 이미지를 만드는게 좋습니다. 그리고 유저가 사용중인 기기의 해상도를 아는 것도 도움이 됩니다. iPhone 에서 사이트를 열었을 때, 엄청나게 큰 헤더 이미지를 보여줄 필요가 없습니다. CSS media queries 를 사용해서 작은 이미지 파일을 사용하게 할 수 있습니다.
@media only screen
and (min-device-width : 320px)
and (max-device-width : 480px) {
.header {
background-image: url(../images/background_400x200.jpg);
}
}
Compression (압축)
적당한 크기의 이미지를 보여주는 것만으로 충분하지 않을 때가 있습니다. 어떤 파일 포맷들은 손실 없이 압축을 할 수가 있습니다. 압축하는데 사용할 수 있는 툴들이 많이 있습니다. 포토샵만 해도 Save for Web and Devices 기능을 제공하죠
이 화면에 많은 기능들이 있습니다. 하지만 가장 중요한 것은 Quality
옵션입니다. 80% 정도로 설정하면 파일 사이즈를 엄청나게 줄일 수 있습니다.
물론 프로그래밍 상으로 압축을 할 수도 있지만, 개인적으로는 포토샵에서 압축하는 것을 선호합니다. PHP 로 압축하는 방법은 다음과 같습니다:
function compressImage($source, $destination, $quality) {
$info = getimagesize($source);
switch($info['mime']) {
case "image/jpeg":
$image = imagecreatefromjpeg($source);
imagejpeg($image, $destination, $quality);
break;
case "image/gif":
$image = imagecreatefromgif($source);
imagegif($image, $destination, $quality);
break;
case "image/png":
$image = imagecreatefrompng($source);
imagepng($image, $destination, $quality);
break;
}
}
compressImage('source.png', 'destination.png', 85);
Sprites
서버로의 요청 횟수를 줄여서 사이트의 체감 반응 속도를 높일 수 있습니다. 이미지 하나 하나를 다운 받기 위해, 서버로 요청을 해야 합니다. 여러개의 이미지를 하나로 합칠 수 있으면, 서버로 파일 요청을 한번만 해도 되게 됩니다. 여러개의 이미지를 담은 이미지를 sprite(스프라이트) One 라고 부릅니다. background-position
CSS 스타일을 이용해서 스파라이트에서 원하는 위치를 보여줍니다. Twitter Bootstrap도 내부 아이콘을 표현하기 위해서 스프라이트를 사용합니다:
그리곤 CSS 에서 이 스프라이트 내의 특정 영역을 다음과 같이 보여줍니다:
.icon-edit {
background-image: url("../img/glyphicons-halflings-white.png");
background-position: -96px -72px;
}
Caching
브라우저의 캐슁을 잘 이용하세요. 개발중에는 캐슁기능 때문에 골탕을 먹기도 하지만요, 사이트의 속도를 높이는데는 아주 중요한 기능입니다. 모든 브라우저는 이미지, 자바스크립트 그리고 CSS 파일을 캐슁합니다. 캐슁을 설정하는 방법이 여러가지가 있는데, 자세한 방법을 보시려면 이 문서 를 보시기를 추천합니다. 일반적으로 헤더에 원하는 값을 설정해서 캐슁 방식을 조절할 수 있습니다:
$expire = 60 * 60 * 24 * 1;// seconds, minutes, hours, days
header('Cache-Control: maxage='.$expire);
header('Expires: '.gmdate('D, d M Y H:i:s', time() %2B $expire).' GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
Prefetching (미리 다운 받기)
HTML5 가 점점 진보하고 있습니다.[prefetching
][6] 라는 기능이 있어서, 브라우저에게 어떤 파일들이 앞으로 곧 필요하게 될테니 다운받으라고 명령하는 역활을 합니다.
Data URI Scheme / Inline Images
몇년전에 간단한 web page, 를 만들었습니다. HTML 파일 하나로 페이지를 만들어야 했고, 몇개의 이미지를 첨부할 필요가 있었습니다. Data URI scheme 를 이용해서 이미지를 base64 encoding 된 스트링으로 변환해서 img 의 src 속성으로 설정하였습니다:
이 방법을 이용하면, 이미지가 HTML 에 포함되어, 서버로의 HTTP 요청을 한 회 줄일 수 있습니다. 물론 이미지가 크다면 변환된 스트링도 정말 길겁니다. PHP 로 이미지를 base64 스트링으로 변환하려면 다음과 같이 할 수 있습니다:
$picture = fread($fp,filesize($file));
fclose($fp);
// base64 encode the binary data, then break it
// into chunks according to RFC 2045 semantics
$base64 = base64_encode($picture);
$tag = '';
$css = 'url(https://t1.daumcdn.net/cfile/tistory/2370DF3956E79D5818"pun" style="color: rgb(102, 102, 0);">.str_replace("
", "", $base64).'); ';
위 방법이 유용할 때가 있는데, IE 에서는 잘 작동하지 않을 때가 있을 수 있으니 조심하세요.
CSS
CSS 는 코딩이라고 생각합니다. 스타일들을 정리하고, 역활에 따라 구분을 짓고, 그들 사이의 관계를 설정해야 합니다. 전 CSS 정리가 매우 중요하다고 생각합니다. 웹의 각 부분들이 자신만의 잘 분리된 스타일을 갖게 됩니다. CSS 를 여러 파일들로 분리하는 것은, 정리하는데 도움이 되기도 하지만, 나름의 문제점들도 있습니다.
우리는@import
문을 사용하는 것이 그닥 좋지 않다는 것을 알고 있습니다. 왜나면 @import
하나마다 서버로 요청을 한번씩 해야 되기 때문입니다. 그리고 브라우저는 모든 스타일 파일을 다운받기 전에는 유저에게 아무것도 보여주지 않습니다. CSS 파일이 없거나, 크기가 크면, 유저가 화면에 무엇인가 보기까지 오랜 시간을 기다려야 할 수 있습니다.
Use CSS Preprocessors
CSS preprocessor 를 사용하면 위 문제를 해결할 수 있습니다. 파일들을 여러개로 분리해서 작업하고, 나중에 하나의 CSS 파일로 합쳐서 유저에게 전달할 수 있습니다. CSS preprocessor 는 변수, 중첩 블락, mixin 과 상속등 다양한 기능을 제공합니다. CSS preprocessor 를 사용해도 코딩은 일반 CSS 와 매우 비슷하게 생겼습니다. 하지만 더 관리하기가 편해집니다. Sass, LESS, Stylus. 등이 유명합니다. 다음은 LESS 의 예제입니다:
.position(@top: 0, @left: 0) {
position: absolute;
top: @top;
left: @left;
text-align: left;
font-size: 24px;
}
.header {
.position(20px, 30px);
.tips {
.position(10px, -20px);
}
.logo {
.position(10px, 20px);
}
}
는 다음과 같이 변경됩니다.
.header {
position: absolute;
top: 20px;
left: 30px;
text-align: left;
font-size: 24px;
}
.header .tips {
position: absolute;
top: 10px;
left: -20px;
text-align: left;
font-size: 24px;
}
.header .logo {
position: absolute;
top: 10px;
left: 20px;
text-align: left;
font-size: 24px;
}
버튼을 위한 스타일이 있으면, 텍스트 칼러만 바꾼 버튼을 만들수도 있습니다:
.button {
border: solid 1px #000;
padding: 10px;
background: #9f0;
color: #0029FF;
}
.active-button {
.button();
color: #FFF;
}
효율적인 CSS
대부분의 개발자들은 CSS 의 속도(효율성) 에 대해서는 생각하지 않습니다. CSS 에 따라 페이지가 보여지는데 걸리는 시간이 더 걸리기도 덜 걸리기도 합니다. 재미있는 사실중 하나는, 브라우저가 CSS selector 를 오른쪽에서 왼쪽으로 읽습니다.
body ul li a {
color: #F000;
text-decoration: none;
}
… 위 코드는 매우 느립니다. 왜냐면 브라우저가 <a>
태그를 모두 찾은 후 그 부모들이 조건에 맞는지 검사하는 검사하는 식으로 동작하기 때문입니다. 또한 ID, class, tag, universal(*) 순으로 효율성이 줄어든다는 것도 알아 두시면 좋습니다. tag 로 스타일을 적용한 원소들 보다, id 로 스타일 적용된 것들이 빨리 그려지게 됩니다. 물론 모든 원소에 id 를 추가할 필요는 없지만, 이 지식이 도움이 될 때가 분명 있습니다.
ul #navigation li {
background: #ff0232;
}
ul
을 제거하는 것이 좋습니다. 왜냐면 #navigation
은 페이지 전체에 하나밖에 없기 때문입니다.
body .content p {
font-size: 20px;
}
.content
는 body
의 자식이기 때문에 body 가 굳이 필요 없습니다.
developers.google.com 와 css-tricks.com 에 이와 비슷한 더 많은 팁들이 있습니다.
File Size
모든 CSS 를 다운받기 전에는 유저가 아무것도 볼 수 없기 때문에, CSS 용량은 작을수록 좋습니다. 사이즈를 줄이기 위한 팁들을 소개합니다:
비슷한 스타일을 합친다:
.header {
font-size: 24px;
}
.content {
font-size: 24px;
}
… 다음과 같이 변경합니다:
.header, .content {
font-size: 24px;
}
다음과 같은 코드 대신에:
.header {
background-color: #999999;
background-image: url(../images/header.jpg);
background-position: top right;
}
짧게 쓸 수 있습니다:
.header {
background: #999 url(../images/header.jpg) top right;
}
CSSOptimiser 와 Minifycss 등의 툴을 이용해서 공백을 제거하여 파일 사이즈를 줄일 수 있습니다. 서버에서 파일 사이즈를 줄여서 유저에게 작은 CSS 를 전달하는 것이 좋습니다.
CSS 파일을 안에 넣기
.css
파일을 head
안에 넣으세요. 브라우저가 그 파일들을 먼저 다운 받습니다.
JavaScript
HTTP 요청 줄이기
CSS 와 동일합니다. 서버로의 요청을 줄이세요. 대부분의 경우 자바스크립트 파일을 로딩중이라고 페이지 로딩이 중단되지는 않습니다. 페이지의 기능들이 동작을 안할 수는 있지만요.
Minify Your Code
자바스크립트의 크기를 줄여주는 라이브러리들이 있습니다. 물론 개발중에는 사용하지 마세요. 대부분의 툴들은 변수이름을 바꾸고, 코드를 한줄로 바꿔버려서 디버깅하기 매우 어렵게 됩니다.
자바스크립트 자체에는 모듈이라는 개념이 없습니다. 이를 해결하기 위한 라이브러리들이 있습니다. 다음 예제는 이 링크에서 가져왔습니다.
<!DOCTYPE html>
<html>
<head>
<title>My Sample Project</title>
<!-- data-main attribute tells require.js to load
scripts/main.js after require.js loads. -->
<script data-main="scripts/main" src="scripts/require.js"></script>
</head>
<body>
<h1>My Sample Project</h1>
</body>
</html>
Inside of main.js
, you can use require()
to load any other scripts you need:
require(["helper/util"], function(util) {
//This function is called when scripts/helper/util.js is loaded.
//If util.js calls define(), then this function is not fired until
//util's dependencies have loaded, and the util argument will hold
//the module value for "helper/util".
});
Use Namespaces
코딩을 정리하는 주제로 얘기하자면 namespace 에 대해 빼놓을 수가 없겠죠. 자바스크립트 자체에는 namespace 개념이 없습니다. 하지만 간단히 해결할 수 있습니다. MVC 를 구현하기 위해 다음처럼 클래스를 구현했다고 합시다
var model = function() { ... };
var view = function() { ... };
var controller = function() { ... };
위 코드 그대로 사용하면, 프로젝트의 다른 파일과 겹칠 확률이 높습니다. 하지만 객체(namespace) 를 만들어서 모아둔다면, 그럴 확률이 줄어들 겁니다.
var MyAwesomeFramework = {
model: function() { ... },
view: function() { ... },
controller: function() { ... }
}
Design Patterns 활용하기
이미 만들어져 있는 것을 새로 만드려고 하지마세요. 자바스크립트는 매우 많은 사람들이 쓰고 있고, 많은 것들이 만들어져 있습니다. 디자인 패턴은, 자주 발생하는 문제들에 대한 많은 사람들이 해결책을 정리해 놓은 것입니다. 남이 제시한 해결책을 사용하는 것이 도움이 될 때가 많이 있습니다. 몇가지 예를 들어 보겠습니다:
Constructor Pattern
특정 타입의 객체를 만들 때 이 패턴을 사용하세요:
var Class = function(param1, param2) {
this.var1 = param1;
this.var2 = param2;
}
Class.prototype = {
method:function() {
alert(this.var1 %2B "/" %2B this.var2);
}
};
다음 처럼 할 수도 있습니다:
function Class(param1, param2) {
this.var1 = param1;
this.var2 = param2;
this.method = function() {
alert(param1 %2B "/" %2B param2);
};
};
var instance = new Class("value1", "value2");
Module Pattern
모듈 패턴은 private 과 public 함수를 만들게 해줍니다. 예를들어 다음 코드에서 _index
와 privateMethod
는 private이고 increment
와 getIndex
은 public 입니다.
var Module = (function() {
var _index = 0;
var privateMethod = function() {
return _index * 10;
}
return {
increment: function() {
_index %2B= 1;
},
getIndex: function() {
return _index;
}
};
})();
Observer Pattern
이벤트를 받거나 전달할 때, 이 패턴을 보게됩니다. 옵저버들은 다른 특정 객체에 관심을 갖고 있습니다. 이벤트가 발생하면 옵저버들이 이벤트에 대해 알게 됩니다. 아래 예제 는 어떻게 Users
객체에 옵저버를 추가할 수 있는지 보여줍니다:
var Users = {
list: [],
listeners: {},
add: function(name) {
this.list.push({name: name});
this.dispatch("user-added");
},
on: function(eventName, listener) {
if(!this.listeners[eventName]) this.listeners[eventName] = [];
this.listeners[eventName].push(listener);
},
dispatch: function(eventName) {
if(this.listeners[eventName]) {
for(var i=0; i<this.listeners[eventName].length; i%2B%2B) {
this.listeners[eventName][i](this);
}
}
},
numOfAddedUsers: function() {
return this.list.length;
}
}
Users.on("user-added", function() {
alert(Users.numOfAddedUsers());
});
Users.add("Krasimir");
Users.add("Tsonev");
Function Chaining Pattern
이 패턴은 public 인터페이스를 정리하는데 도움이 되는 패턴입니다. 코드의 가독성을 높일 수 있습니다:
var User = {
profile: {},
name: function(value) {
this.profile.name = value;
return this;
},
job: function(value) {
this.profile.job = value;
return this;
},
getProfile: function() {
return this.profile;
}
};
var profile = User.name("Krasimir Tsonev").job("web developer").getProfile();
console.log(profile);
JavaScript 의 패턴에 관한 아주 좋은 책 을 추천합니다.
Assets-Pack
글이 막바지에 다다르고 있습니다. 서버에서 CSS 와 JavaScript 를 관리하는 방법에 대한 얘기를 하고 싶습니다. 파일을 합치고, 사이즈를 줄이고(minification), 컴파일(preprocessor 등) 하는 것을 서버 코드의 일부로 구현하는 경우가 많이 있습니다.
제가 최근에 작업한 프로젝트에서, assets-pack 라는 툴을 사용했습니다. 매우 유용한 것 같아, 제가 어떻게 이 툴을 이용했는지 자세히 설명드리고자 합니다. 이 라이브러리는 개발중에만 사용하도록 만들어졌습니다.
기본적으로 여러 리소스 파일들(CSS, JS) 이 변경되면, 이 툴이 알아서, 모든 파일을 하나의 파일로 합쳐줍니다. 파일을 하나만 전송해도 되니, 속도가 빨라집니다. 이 툴 이외에 서버에 다른 기능을 추가할 필요가 없기 때문에 개발하는 동안 편리하게 사용할 수 있습니다.
[assets-pack
][23] 설치방법입니다.
Installation
이 툴은 Nodejs 를 사용합니다. 없으신 분들은 nodejs.org/download 에서 파일을 받으세요. 그리고 다음과 같이 합니다:
npm install -g assetspack
Usage
Command Line
이 툴은 JSON 설정파일을 사용합니다.
assets.json
파일을 만들고 같은 디렉토리에서 다음 명령어를 실행합니다:
assetspack
설정파일의 이름이 다르거나 다른 디렉토리에 있다면 다음과 같이 하세요:
assetspack --config [path to json file]
In Code
var AssetsPack = require("assetspack");
var config = [
{
type: "css",
watch: ["css/src"],
output: "tests/packed/styles.css",
minify: true,
exclude: ["custom.css"]
}
];
var pack = new AssetsPack(config, function() {
console.log("AssetsPack is watching");
});
pack.onPack(function() {
console.log("AssetsPack did the job");
});
Configuration
설정파일은 다음과 같이 생겼습니다:
[
(asset object),
(asset object),
(asset object),
...
]
Asset Object
기본적인 asset object 는 다음과 같이 생겼습니다:
{
type: (file type /string, could be css, js or less for example),
watch: (directory or directories for watching /string or array of strings/),
pack: (directory or directories for packing /string or array of strings/. ),
output: (path to output file /string/),
minify: /boolean/,
exclude: (array of file names)
}
pack
property 가 반드시 필요한 것은 아닙니다. 생략하면 watch
와 같은 값을 갖게 됩니다. minify 는 기본값이 false 입니다.
몇가지 예입니다:
Packing CSS
{
type: "css",
watch: ["tests/data/css", "tests/data/css2"],
pack: ["tests/data/css", "tests/data/css2"],
output: "tests/packed/styles.css",
minify: true,
exclude: ["header.css"]
}
Packing JavaScript
{
type: "js",
watch: "tests/data/js",
pack: ["tests/data/js"],
output: "tests/packed/scripts.js",
minify: true,
exclude: ["A.js"]
}
Packing .less
Files
.less
압축(packing)은 조금 다릅니다. less 파일에 대해서는 pack
property 가 필수입니다. 다른 모든 .less
파일을 import 하여야 하고 exclude
property 는 제공되지 않습니다.
{
type: "less",
watch: ["tests/data/less"],
pack: "tests/data/less/index.less",
output: "tests/packed/styles-less.css",
minify: true
}
문제가 있으면 Github 저장소의 tests/packing-less.spec.js
를 보세요.
Packing Other File Formats
assets-pack
툴을 다른 파일 포맷에도 사용할 수 있습니다. HTML 을 합쳐서 하나로 만들 수도 있습니다:
{
type: "html",
watch: ["tests/data/tpl"],
output: "tests/packed/template.html",
exclude: ["admin.html"]
}
다만 이 경우 minification 은 지원되지 않습니다.
Conclusion
프론트 웹 개발자로서, 사용자들을 위해 성능을 높이기 위해 노력해야 합니다. 위에 나열한 팁들이 완벽한 리스트는 아니지만, 제가 개인적으로 사용해본 방법들입니다. 여러분들이 사용하는 다른 팁들도 공유 부탁합니다.