기술자료

DBMS, DB 구축 절차, 빅데이터 기술 칼럼, 사례연구 및 세미나 자료를 소개합니다.

효율적인 자바스크립트 merge/minify 노하우 : Wro4j와 Custom JSTL 활용하기

기술자료
DBMS별 분류
Etc
작성자
dataonair
작성일
2014-04-10 00:00
조회
5367



효율적인 자바스크립트 merge/minify 노하우

Wro4j와 Custom JSTL 활용하기



모바일 웹서비스를 개발하다 보면 Front-end 성능을 고려하지 않을 수 없다. 성능 향상을 위한 practice 중 하나는 css, js 같은 리소스들을 merge하고 minify해 사용하는 것이다. 성능향상을 위해 필수적인 과정이라고는 하지만 merge/minify를 위한 설정을 하고, 이러한 개발환경에서 작업하다 보면 불편한 점이 생기기 마련이다. 필자도 이에 불편함을 겪었고 이를 개선하기 위해 고민했었다. 필자는 Wro4j(Web Resource Optimizer for Java)라는 오픈소스(아파치 라이선스 2.0) 라이브러리와 함께 Custom JSTL을 만들어 문제를 개선할 수 있었다.



모바일 웹페이지 개발자라면 Front-end 성능 최적화를 위해 js 파일 merge/minify를 고려해야 한다. css도 고려할 수 있겠으나 이 글에서는 js에 대해서만 이야기할 것이다. 운 좋게 js 파일 하나로 merge/minify해 사용할 수 있다면 간단하겠지만, js 파일들의 사이즈가 너무 크거나 페이지마다 다른 종류의 js를 조합해서 사용해야 하는 경우라면 merge/minify할 js 파일들을 관리하기 위한 비용이 적잖이 들어갈 것이다.



Wro4j와 Custom JSTL의 필요성

필자가 근무하는 회사의 개발환경에서도 여러 js 파일들을 merge/minify하기 위해 사용하고 있는 방법이 있었지만 관리하기 불편한 부분이 있어 이를 개선하고자 고민했다. 해결책은 wroj4와 Custom JSTL을 활용하는 것이었다. 이 글에서는 다소 생소할 수 있는 Wro4j 라이브러리를 소개하고, Custom JSTL을 만들어 효율적으로 merge/minify된 js를 사용하는 방법을 살펴볼 것이다.
Wro4j는 js를 merge하고 miminfy하는 오픈소스 도구다(code.google.com/p/wro4j/). Wro4j는 merge할 파일들을 관리하고, 기존에 존재하는 오픈 라이브러리들을 활용해 minify를 할 수 있도록 확장성 있게 설계됐다. 그리고 mege/minify 과정이 build-time, run-time 모두 가능하도록 돼 있어 사용자의 환경에 맞게 선택할 수 있다.

필자는 Wro4j를 통해 merge만 하고, merge가 끝난 후 별도의 라이브러리로 minify 과정을 거치게 했다. 그 이유는 Wro4j에서 사용하는 minify 라이브러리를 사용하면 오류가 발생했기 때문이다. 그래서 기존 minify 라이브러리를 사용할 수밖에 없었다. Wro4j에서 제공하는 mnify 라이브러리를 적용했을 때 에러가 나더라도 다른 minify 라이브러리를 사용하면 되기 때문에 실망할 필요는 없다. merge할 리소스를 관리하고 maven plugin을 제공해주는 것만으로도 Wro4j는 충분히 사용할 만하다. 그렇다면 JSTL은 왜 사용해야 할까 필자가 JSTL을 고려했던 이유는 다음의 네 가지를 위해서였다.

1) 디버깅(debugging) 편의성

개발자라면 로컬 개발을 할때 merge/minify 이전의 js 파일로 개발을 하고 싶을 것이다. 개발 도중 수정이 필요한 부분을 찾기 위해서 merge/minify된 js 파일을 열어보고 디버깅한다는 것이 쉽지 않기 때문이다.



2) 개발 편의성

merge/minify된 js 파일을 사용하기 위해서는 < script src=”/auto/generated/location/merge/minify.js”/>처럼 사용해야 한다. 이 방법은 로컬 개발환경에서 merge/minify된 파일이 존재하지 않는다면 사용할 수 없다. js 코드를 수정하고 확인할 때마다 js를 merge/minify하기 위한 빌드를 매번 실행해야 한다면 개발속도가 크게 느려질 것이다.

3) js 더미 파라미터(dummy parameter) 자동생성

1, 2번이 주된 목적이라면 3, 4번은 부가적인 추가 기능들이다. js를 사용할 때 보통 cache ttl을 길게 잡기 때문에 사용자 브라우저에 캐시된 js를 업데이트하기 위해서는 의미없는 더미 파라미터를 갱신해 캐시된 js를 업데이트하게 한다. merge된 js를 사용할 때 merge에 포함된 하나의 js가 수정되면 더미 파라미터를 갱신해야 한다. 이 과정을 개발자가 매번 수정하기는 힘들기 때문에 빌드되는 시점에 항상 새로운 더미 파라미터를 추가해 주는 기능이 필요했다.

4) lazy 로딩

모바일 환경은 Front-end 속도에 민감하기 때문에 페이지 로딩 시 필요치 않는 js라면 onLoad 이벤트 후에 로딩하거나, async하게 로딩하는 방법을 사용할 것이다. merge된 js가 성능 이슈로 onLoad 이후에 로딩돼야 할 경우 이를 간단하게 처리할 수 있는 방법이 필요했다.



이 네 가지 이슈를 해결하기 위해 Custom JSTL을 만들어 사용하기로 했다. Custom JSTL에 대해 설명하기에 앞서 먼저 Wro4j에 대해 조금 더 알아보자.



자바스크립트 merge/minify를 위한 Wro4j

Wro4j는 css, js같은 웹 리소스(web resource)를 관리하는 데 필요한 JsHint, CssLint, JsMin, Google Closure compressor, YUI Compressor같은 도구를 사용할 수 있도록 도와주는 자바 오픈소스 프로젝트로, build-time 또는 run-time에 merge나 minify 작업을 진행할 수 있도록 도와준다. Web Resource Optimizer for Java(code.google.com/p/wro4j/)를 참고하면 자세한 내용을 확인할 수 있다.
wroj4는 run-time과 build-time 두 가지 모두 가능케 한다. run-time이라도 매번 merge/minify 작업을 하는 것은 아니다. 캐시 기능을 제공하기 때문에 한번 mege/minify된 리소스들은 cache ttl 기간 동안 메모리에서 재사용이 가능하다.


tech_img1214.jpg


실제 서비스에 적용하기 위해서는 안정성이 가장 먼저 고려돼야 하기 때문에 필자는 run-time보다 build-time에 merge/ minify를 진행하기로 했다.
Wro4j는 xml, json, groovy의 세 가지 표기법으로 merge/ minify할 리소스들을 관리한다. 그 중 xml 방식을 살펴보자.


<리스트 1> merge/minify 대상 resource를 관리하는 wro.xml < groups xmlns="http://www.isdc.ro/wro">
< group name="g1">
< js>classpath:com/mysite/resource/js/1.js< /js>
< css>classpath:com/mysite/resource/css/1.css< /css>
< group-ref>g2< /group-ref>
< /group> < group name="g2">
< js>/dwr/engine.js< /js>
< group-ref>g3< /group-ref>
< css>classpath:/static/css/2.css< /css>
< js>classpath:/static/*.js< /js>
< /group> < group name="g3">
< css>/static/css/style.css< /css>
< css>/static/css/global/*.css< /css>
< js>/static/js/**< /js>
< js>http://www.site.com/static/plugin.js< /js>
< /group>

<리스트 1>은 Wro4j 홈페이지(code.google.com/p/wro4j/wiki /GettingStarted)에 있는 소스 코드다. wro.xml에는 groups라는 최상위 엘리먼트와 merge되는 그룹을 지정하는 group 엘리먼트가 있다. group 엘리먼트에는 css와 js의 위치를 지정하는 js/css 엘리먼트와 다른 그룹을 참조하는 group-ref 엘리먼트가 있다. js/css 엘리먼트 안에는 classpath, web application context의 상대경로, url의 세 가지 방법으로 js의 위치를 지정할 수 있다. group-ref가 있어 공통으로 merge되는 파일들을 g3처럼 정의해 두고 다른 그룹에서 참조해 사용하면 효율적일 것이다. 이렇게 설정된 group은 다음에 살펴볼 maven plugin에서 지정한 위치에 ‘그룹명.js’ 또는 ‘그룹명.css’로 생성된다.


<리스트 2>는 build-time 시에 merge/minify 과정을 진행하기 위한 maven plugin 설정이다(출처 : code.google.com/p/wro4j/ wiki/MavenPlugin).


<리스트 2> maven plugin 설정< plugins>
< plugin>
< groupId>ro.isdc.Wro4j< /groupId>
< artifactId>Wro4j-maven-plugin< /artifactId>
< version>${Wro4j.version}< /version>
< executions>
< execution>
< phase>compile< /phase>
< goals>
< goal>run< /goal>
< /goals>
< /execution>
< /executions>
< configuration>
// 실행할 그룹들을 지정. all은 전제 groups 대상
< targetGroups>all< /targetGroups>
// 사이즈 최소화 작업 여부. 기본값은 true
< minimize>true< /minimize>
// 생성될 js 파일의 위치
< jsDestinationFolder>d:/static/js/< /jsDestination Folder>
// web application context 위치. 기본값은 ${basedir}/src/main/webapp/
< contextFolder>${basedir}/src/main/webapp/< /context Folder>
// wro.xml 파일 위치
< wroFile>${basedir}/src/main/webapp/WEB-INF/wro.xml< /wroFile>
// WroManagerFactor 설정. Wro4j에서 제공하는 Factory를 사용할 수 있음
// 아래 설정처럼 사용자가 구현해 사용할 수도 있다.
< wroManagerFactory>com.mycompany.MyCustomWro ManagerFactory< /wroManagerFactory>
// resource validation. false로 설정하면 resource들이 없을 때 build fail 발생
< ignoreMissingResources>false< /ignoreMissing Resources>
< /configuration>
< /plugin>
< /plugins>

tech_img1213.png

Wro4j framework의 핵심 컴포넌트인 WroManager에 대해 간단히 소개하고자 한다. WroManager는 Wro4j 프레임워크의 핵심으로 <그림 1>의 과정을 거쳐 merge/minify 작업을 진행하게 된다.




효율적인 js 사용을 위한 Custom JSTL 만들기

앞서 Custom JSTL을 만드는 목적 네 가지를 설명했다. 이를 고려해 만들게 될 Custom JSTL이 JSP에서 사용될 모습은 다음와 같다.

< wro:js group=“g1” lazy=“true”/>

이는 wro.xml의 g1 그룹으로 merge된 js를 로딩할 것이고, lazy로딩을 하겠다는 의미다. 그리고 실제 html 페이지에서는 다음과 같이 변환될 것이다.


<리스트 3> Custom JSTL을 이용해 merge/minify된 js 사용하기개발모드
< script language="javascript" src="/js/source/a.js"> < /script>
< script language="javascript" src="/js/source/b.js"> < /script>
(개발 모드에서는 g1그룹에 포함된 js들의 목록이 나열된다.
개발 모드는 성능을 고려하지 않아도 되기 때문에 lazy로딩 처리를 하지 않았다.)

서비스모드
lazyLoad("/js/gerated/source/g1.jsv=20140101125959");
(자바스크립트를 lazy로딩하는 lazyLoad라는 함수가 이미 존재한다고 가정)< wro:js group="g1" lazy="false"/>
(lazy로딩 여부를 false로 세팅하면 서비스모드에서는 다음과 같이 변환될 것이다.)
< script language="javascript" src='/js/gerated/ source/g1.jsv=20140101125959'>

다음은 taglib 정의다. Custom JSTL에서 group 속성은 필수로, lazy로딩 여부는 선택적으로 사용할 수 있도록 taglib을 만들어 보자.


<리스트 4> Custom JSTL을 위한 taglib 정의< taglib>
< tlib-version>1.0< /tlib-version>
< jsp-version>2.1< /jsp-version>
< short-name>Wro< /short-name>
< uri>http://sample.domain.com/taglibs< /uri>
< description>Wro Custom Tags< /description>
< tag>
< name>js< /name>
< tag-class>com.sample.domain.wro.WroJsTag< /tag-class>
< body-content>empty< /body-content>
< attribute>
< name>group< /name>
< required>true< /required>
< /attribute>
< attribute>
< name>lazyload< /name>
< required>false< /required>
< /attribute>
< /tag>
< /taglib>

이제 개발모드 여부와 merge/minify된 파일의 위치를 properties 파일에 설정한다.

<리스트 5> wro.properties 설정#개발모드 여부
debug=false
# merge된 js 파일의 위치
generatedJsPath=/js/generated

이제 Custom JSTL을 만들기 위한 코드를 살펴보자. Custom JSTL을 만들어 본 경험과 자바로 XML 파싱을 해본 경험이 있다면 코드를 이해하는 데 도움이 될 것이다. 소스 코드는 Spring 3.x 프레임워크 버전에서 개발됐다.

- Group.java : group 내 wro.xml의 group 엘리먼트를 모델화한 Class

- Item.java : group 내부의 js/css/group-ref를 모델화한 Class로 js/css/group-ref 타입을 저장하는 type과 resource 위치인 url 속성을 가지고 있다.

- WroJsTag.java : Wro 태그 라이브러리를 처리하는 Class

- WroRepository.java : wro.xml을 파싱해 List 타입의 속성을 가지고 있다. debug 모드일 때 분리된 js 파일들을 노출하기 위해 필요하다.

- WroTimestamp.java : 더미 파라미터를 노출하기 위해 timestamp 정보를 가지고 있다.



그리고 wro.properties 정보를 읽기 위해 bean 설정도 필요하다.


<리스트 6> Custom JSTL을 만들기 위해 사용되는 자바 파일 목록< bean id="wroProperties" class="org.springframework.
beans.factory.config.PropertiesFactoryBean">
< property name="location" value="classpath:wro. properties" />
< /bean>



<리스트 7> Group.java 소스 코드public class Group {
// wro.xml에 정의된 group name
private String name;
// group 내에 정의된 js / group-ref 목록
private List itemList; public String getName() {
return name;
} public List getItemList() {
return itemList;
} public void setName(String name) {
this.name = name;
} public void setItem(List itemList) {
this.itemList = itemList;
} public void addItem(Item item) {
if (itemList == null) {
itemList = new ArrayList();
}
itemList.add(item);
}
}



<리스트 8> Item.java 소스 코드public class Item {
// js, group-ref 엘리먼트 타입
private String type;
// resource의 위치
private String url; public String getType() {
return type;
} public String getUrl() {
return url;
} public void setType(String type) {
this.type = type;
} public void setUrl(String url) {
this.url = url;
}
}



<리스트 9> WroTimestamp.java 소스 코드public class WroTimestamp {
// 더미 파라미터에 추가될 timestamp 포맷
private static SimpleDateFormat sdf =
new SimpleDateFormat("yyyyMMddhhmmss", Locale.KOREA);
private static String timestamp = sdf.format(new Date());
public static String getTimestamp() {
return timestamp;
}
}



<리스트 10> WroRepository.java 소스 코드import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;@Component
public class WroRepository {
private static final Log LOG = LogFactory.getLog(WroRepository.class);
private List groupList = new ArrayList(); public WroRepository() throws IOException {
// wro.xl 파일을 읽음
Resource rsrc = new ClassPathResource(“wro.xml”);
File fXmlFile = rsrc.getFile();
try {
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
// XML Dom 파싱을 함
Document doc = dBuilder.parse(fXmlFile);
// group 엘리먼트를 읽어 Group 객체를 만듦
NodeList nList = doc.getElementsByTagName("group");
for (int temp = 0; temp < nList.getLength(); temp++) {
Node nNode = nList.item(temp);
if (nNode.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
Element el = (Element)nNode;
String groupName = el.getAttribute("name"); NodeList list = nNode.getChildNodes(); Group group = new Group();
group.setName(groupName);
// group 엘리먼트에 하위의 js, group-ref 엘리먼트를 읽어 Item 객체를 만듦
for (int t = 0; t < list.getLength(); t++) {
Node node = list.item(t);
if (node.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
Item item = new Item();
item.setType(node.getNodeName());
item.setUrl(node.getTextContent());
group.addItem(item);
}
getGroupList().add(group);
} } catch (Exception e) {
LOG.error(e, e);
}
} public void setGroupList(List groupList) {
this.groupList = groupList;
} public List getGroupList() {
return groupList;
}
}



<리스트 11> WroJsTag.java 소스 코드import java.io.IOException;
import java.util.List;
import java.util.Properties;import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.tagext.TagSupport;import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;public class WroJsTag extends TagSupport { private static final String LOAD_JAVASCRIPT
= "< script type='text/javascript' src='%s.jsv=%s'>< /script>";
private static final String LAZY_LOAD_JAVASCRIPT
= "< script>lazyloadjs('%s.jsv=%s');< /script>";
private static final String DEBUG_JAVASCRIPT
= "< script type='text/javascript' src='%sv=%s'>< /script>";
private static final Log LOG = LogFactory.getLog(WroJsTag.class);
private String group;
private boolean lazyload = false;
private Boolean isDebugMode;
private String path; @Override
public int doStartTag() throws JspException { try {
// property 정보에서 debug mode 여부를 가져온다.
String debug = getWroProperties().getProperty("debug");
if (“true”.equals(debug)) {
isDebugMode = true;
} else {
isDebugMode = false;
}
JspWriter out = pageContext.getOut(); if (isDebugMode) {
// debug 모드일 때는 merge하기 전 js 목록을 노출
WroRepository wroRepository = getWroRepository();
List groupList = wroRepository.getGroup List();
for (Group tmpGroup : groupList) {
if (tmpGroup.getName().equals(group)) {
writeJs(out, tmpGroup, groupList);
}
}
} else {
// debug 모드가 아니면 lazy 로드 여부에 따라 자바스크립트 로딩 스크립트 출력
setPath();
if (isLazyload()) {
out.println(String.format(LAZY_LOAD_JAVASCRIPT, path + group, WroTimestamp.getTimestamp()));
} else {
out.println(String.format(LOAD_JAVASCRIPT, path + group, WroTimestamp.getTimestamp()));
}
} } catch (IOException e) {
LOG.error(e, e);
}
return SKIP_BODY;
}

void writeJs(JspWriter out, Group group, List groupList) throws IOException {
for (Item item : group.getItemList()) {
if (item.getType().equals("group-ref")) {
for (Group tempGroup : groupList) {
if (tempGroup.getName().equals(item.getUrl())) {
writeJs(out, tempGroup, groupList);
break;
}
}
} else if (item.getType().equals("js")) {
out.println(String.format(DEBUG_JAVASCRIPT, item.getUrl(), WroTimestamp.getTimestamp()));
}
}
}

// merge/minify된 js 파일이 생성되는 위치를 wro.properties 파일에서 읽어옴
private void setPath() {
if (path == null) {
String temp = (String)getWroProperties().get ("generatedJsPath");
if (!temp.startsWith("/")) {
temp = "/" + temp;
}
if (!temp.endsWith("/")) {
temp = temp + "/";
}
path = temp;
}
} protected WroRepository getWroRepository() {
WebApplicationContext context = WebApplication ContextUtils.
getRequiredWebApplicationContext(pageContext.getServletContext());
return context.getBean("wroRepository", WroRepository.class);
} protected Properties getWroProperties() {
WebApplicationContext context = WebApplicationContextUtils.
getRequiredWebApplicationContext(pageContext.getServletContext());
return context.getBean("wroProperties", Properties.class);
} public void setGroup(String group) {
this.group = group;
} public String getGroup() {
return group;
} public boolean isLazyload() {
return lazyload;
} public void setLazyload(boolean lazyload) {
this.lazyload = lazyload;
}
}



Custom JSTL 사용하기

JSP에 를 한 줄 추가하면, wro.properties debug=true/false 설정에 따라 다른 모습으로 html 페이지가 노출되는 것을 볼 수 있다.


<리스트 12> debug 모드에 따른 변환 debug =true
< script language="javascript" src="/js/source/a.js"> < /script>
< script language="javascript" src="/js/source/b.js"> < /script>

debug =false
lazyLoad('/js/gerated/source/g1.jsv=20140101125959');


필자는 Wro4j와 Custom JSTL을 함께 사용함으로써 좀더 효율적으로 앞서 언급했던 문제를 개선할 수 있었다. 필자와 비슷한 문제를 경험하고 있는 개발자들에게 좋은 팁이 됐으면 좋겠다.