 |  |
|
난이도 : 중급 Bruce Tate, CTO, WellGood LLC
2007 년 8 월 07 일 사용자 관련 콘텐트는 캐싱이 불가능합니다. 사용자의 콘텐트는 약간 미묘한 문제이기 때문입니다. JavaScript와 쿠키를 함께 사용하면 커스텀 사용자 데이터를 디스플레이 할 때에도 페이지 캐싱을 사용할 수 있습니다. 이 글에서 Ruby on Rails의 고급 페이지 캐싱 기능을 설명합니다.
페이지 캐싱에 Rails는 전혀 개입되지 않는다. 어떤 면에서는, 좋은 일일 수 있다. 최상의 성능을 얻을 수 있기 때문이다. Rails는 HTML 페이지를 단 한번 만들고, 이를 디렉토리에 저장하고, 이것에 대해 잊어버린다. 이 때부터, 애플리케이션 서버로 한번도 가지 않고 애플리케이션 서버는 페이지를 제공한다. 성능의 관점에서 볼 때, 페이지 캐싱은 더 없는 축복이다.
필자는 페이지 캐싱을 좋아하고, Rails는 이를 단순하고 깔끔하게 처리한다. 단 한 줄의 코드로, 캐시를 실행할 수 있다. 그 이상의 코드 라인으로는, 파일을 삭제하거나 Rails 고급 API를 사용함으로써, 캐시를 종료할 수 있다. 하지만 문제가 있다. 모든 사이트가 페이지 캐싱을 사용할 수 있는 것은 아니다. 페이지를 보는 사람에 따라서 변하는 페이지의 데이터의 경우, 페이지 캐싱이 불가능 하다. 페이지를 종료해야 할 시기를 결정하는데 어려움이 있다면, 페이지 캐싱을 요구하고 있는지도 모른다.
예를 들어, ChangingThePresent.org(사이드바 참조)는 현재 로그인 된 사용자에 기반하여 변하는 사용자 데이터를 갖고 있다. 그림 1은 최신 홈 페이지의 한 섹션을 보여주고 있다. (이 페이지는 곧 변경될 것이다.) 이 페이지는 비교적 단순한 문제를 드러내고 있다. 사용자가 로그인 했는지 여부를 파악할 수 있다면, Flash, JavaScript, DHTML, 기타 브라우저 기반 코드를 사용하여 뷰를 커스터마이징 할 수 있다. 로그인을 한 사용자는 로그아웃을 하거나 자신의 프로파일을 볼 수 있고, 로그아웃을 한 사용자는 로그인 할 수 있다.
그림 1. ChangingThePresent.org의 로그인 및 로그아웃 뷰
 | |
Real world Rails 시리즈에서는 국제적으로 유명한 저자이자 연사인 Bruce Tate가 Rails 개발의 실체를 속속들이 해부합니다. Bruce Tate는 WellGood, LLC의 CTO로서 ChangingThePresent.org를 디자인, 구현, 관리하고 있습니다. 이 사이트는 자선 기부 포털로서 다양한 방식으로 기부할 수 있습니다. 수십만 사용자들은 ChangingThePresent를 비영리 자선 단체로 인정하고 있고 그 규모와 대중성이 커지고 있습니다.
Rails 애플리케이션을 구현하는 것과 관련한 수십 건의 기술자료들이 있습니다. 본 시리즈에서는 기본적인 블로그를 구현하는 기초를 넘어서, 모든 Rails 사이트가 해결해야 하는 문제들을 짚어봅니다. Rails를 최적화 하는 방법, 더욱 안정적인 사이트를 만드는 방법을 배우게 될 것입니다. 또한, 플러그인들을 추가하여 Rails의 한계를 극복하는 방법도 설명합니다. 본 시리즈의 기술자료를 읽은 후에, 실제로 Rails 사이트가 작동하는 방법을 더욱 잘 이해할 수 있을 것입니다.
|
|
Rails 애플리케이션을 구현하는 것과 관련한 수십 건의 기술자료들이 있습니다. 본 시리즈에서는 기본적인 블로그를 구현하는 기초를 넘어서, 모든 Rails 사이트가 해결해야 하는 문제들을 짚어봅니다. Rails를 최적화 하는 방법, 더욱 안정적인 사이트를 만드는 방법을 배우게 될 것입니다. 또한, 플러그인들을 추가하여 Rails의 한계를 극복하는 방법도 설명합니다. 본 시리즈의 기술자료를 읽은 후에, 실제로 Rails 사이트가 작동하는 방법을 더욱 잘 이해할 수 있을 것입니다.
그림 2. 두 개의 뷰
이 작동은 ChangingThePresent.org만의 것이 아니다. 사용자 경험을 개인화 하기 시작한 순간, 변경되지 못한 Rails 페이지 캐싱의 사용을 제한하지만, 약간의 커스터마이징으로 이 페이지들을 매우 쉽게 저장할 수 있다.
여러 가지 방식들로 이러한 문제들을 해결할 수 있다. 이러한 방법들은 필자 개인적으로는 매우 매력적이라고 생각한다.
- Rails 프레임웍의 제약 조건을 지키면서, 대신 조각(fragment) 캐싱을 사용한다.
- 대부분의 페이지를 로딩한 다음, JavaScript와 Ajax를 사용하여 페이지의 동적인 작은 부분들을 로딩한다. 서버 측 코드는 사용자가 로그인 했는지 여부를 탐지한 다음, Ajax로 적절한 파셜(partial)을 렌더링한다.
- 사용자 로그인 여부 같은 사용자 상태를 클라이언트 측 쿠키에 저장할 수 있다. 그런 다음, 쿠키의 콘텐트에 따라서 JavaScript로 페이지의 모양을 동적으로 바꿀 수 있다.
이 세 가지 방법 중에서, 필자는 세 번째를 가장 선호한다. 첫 번째와 두 번째 방법은 Rails 애플리케이션을 실행해야 한다. 확장성을 원한다면, 정적인 콘텐트를 다루어야 한다. 이 글에서는, 세 번째 방법을 집중적으로 설명하도록 하겠다. ICBM 론치 코드나 신용 카드 번호 같이 민감한 사안의 경우, 이 방법을 사용하지 않기 바란다. 일부 데이터에만 이 솔루션이 빛을 발한다.
Show and tell, 또는 hide and seek?
필자가 홈페이지 캐싱을 처음 수행했을 때, 링크를 JavaScript로 대체하기로 결정했었다. 이 기술을 Show-and-tell로 보면 된다. 로그인을 한 사용자에 대해 여러분이 알고 있는 것을 기반으로, 웹 페이지의 부분들을 JavaScript로 선택적으로 대체하거나 투입함으로써 사용자에게 맞는 부분을 보여줄 수 있다. 좀더 자세히 설명하면, 다음과 같다.
- 모든 사용자들에게 공통인 엘리먼트로만 웹 페이지를 만든다.
- 사용자가 로그인 할 때, 로그인 같은 사용자에 대한 일부 데이터를 쿠키에 저장한다.
- 쿠키 내용에 기반하여 HTML 조각들을 JavaScript에 투입함으로써 나머지 페이지들을 채운다.
ChangingThePresent.org 홈페이지의 경우, 이러한 Show-and-tell 기술은 과잉이다. 로그인 사용자에 기반하여 단 두 개의 링크 세트만 보여줄 수 있기 때문이다. 필자는 제 2의 방법을 선택했는데, 바로 Hide-and-seek이다. 모든 사용자들에게 공통인 모든 페이지 엘리먼트들을 보여주고 디스플레이의 변화하는 부분에 대해서 숨겨진 버전의 데이터를 갖고 있다. 이것이 Hide 부분이다. 그리고 나서, 사용자의 역할에 따라서, JavaScript를 사용하여 이것을 디스플레이용 문서에서 찾는다. 이것이 Seek 부분이다. 데이터의 모든 버전들을 보여주는 것이 과잉이라고 생각하겠지만, 다양한 보안 역할들을 위해 다양한 기능들을 선택적으로 실행할 때 이는 매우 일반적이다. Hide-and-seek은 ChangingThePresent.org 홈페이지에 완벽히 맞는다. 이 기술을 구현하려면 다음과 같이 한다.
- 모든 사용자에 공통인 엘리먼트들로만 웹 페이지를 생성한다.
- 사용자들을 유형별로 나눈다. 각각의 사용자 유형별로 콘텐트 버전을 추가한다. ChangingThePresent.org 홈페이지의 유형은 사용자 로그인과 로그아웃이다. 처음에는 콘텐트를 보이지 않게 한다.
- 사용자가 로그인 하면, 사용자 역할 또는 로그인 상태 같은 사용자 그룹을 구별하는 데이터를 쿠키에 저장한다.
- 사용자가 페이지에 액세스 하면, 사용자 유형에 따라 콘텐트 버전을 선택적으로 보여준다.
hide and seek 구현하기
ChangingThePresent.org 홈페이지의 경우, Hide-and-seek 구현은 매우 간단하다. 이 홈페이지는 사용자 계정과 관련된 링크를 보여주는 부분을 갖고 있다. (그림 1) 이 링크는 사용자가 로그인 했는지의 여부에 따라 변한다. 첫 번째로 해야 할 일은 이 페이지에 대한 모든 공통의 콘텐트를 구현하는 것이다. 여기에서는 설명하지 않겠다. 두 번째 페이지는, 사용자 로그인 여부에 상관 없이, 모든 사용자에 대한 모든 동적 콘텐트를 보여준다.
Listing 1. 동적인 콘텐트의 모든 버전들을 하나의 뷰로 구현하기
<div id='logged_out'>
<%= link_to "login", :controller => 'members', :action => 'login' %>
<br />
<%= link_to "register", :controller => 'members', :action => 'signup' %>
</div>
<div id='logged_in' style="display: none;">
<%= link_to "your profile", :controller => 'profiles', :action => 'show' %>
<%= link_to "logout" , :controller => "members", :action => "logout" %>
</div>
|
You might notice the my profile 링크를 주목하라. 처음에, 이 링크는 사용자 프로파일을 가리켰지만, 홈페이지 캐싱으로 나뉘었다. 대신, 사용자 아이디 없이 인덱스 액션에 대한 링크를 가리킨다. 인덱스 액션은 사용자를 정확한 프로파일 페이지로 돌린다.
Listing 2. 사용자를 정확한 프로파일 페이지로 돌리기
def index
redirect_to my_profile_url
end
|
Listing 2에서, my_profile_url은 사용자의 유형에 기반하여 올바른 프로파일 URL을 결정하는 메소드로서, celebrity, advisor, member 등이 있다. 각각은 개별 프로파일 페이지를 갖고 있다. 이 부분에서, 애플리케이션은 완전한 기능을 수행하지만, logged_in과 logged_out에 각 두 개의 링크, 총 네 개의 링크를 볼 수 있다.
- login
- register
- your profile
- logout
다음 단계는 현재 사용자 유형을 저장하고 있는 쿠키를 캡쳐하는 단계이다. ChangingThePresent의 경우, 로그인 할 때, 현재 로그인 아이디를 갖고 있는 쿠키를 만든다. 그리고 나서, 로그아웃 시간에 쿠키를 삭제한다.
Listing 3. 로그인 및 로그아웃 시 쿠키를 생성 및 삭제하기
def login
if request.post?
self.current_user = User.authenticate(params['user_login'], params['user_password'])
...
if logged_in?
set_cookies
...
end
end
def logout
end
private
def set_cookies
cookies[:login] = current_user.login
cookies[:image] = find_thumb(current_user.member_image)
end
def logout
cookies.delete :login
cookies.delete :image
...
end
|
Listing 3에서, logged_in?은 현재 사용자가 로그인 했다면 true를 리턴하는 개인(private) 메소드이다. 위 Rails 메소드는 사용자가 로그인 할 때 세 개의 쿠키를 만들고, 로그아웃 하면 쿠키를 삭제한다. 데이터에 대해서는 걱정하지 말라. 아직까지 필요하지 않다. 이제는, Rails 프레임웍을 호출하지 않고도, 사용자가 로그인 했는지를 말해줄 수 있다는 것만 이해하라. 쿠키의 만료는 사이트의 종료 정책과 맞아야 한다. 필자는 이제 페이지 캐싱 준비가 되었다.
다음 단계는, 사용자의 쿠키에 기반하여 올바른 엔트리들을 선택적으로 숨기거나 보여주는 단계이다. 다음 JavaScript를 public/javascripts/application.js에 추가했다.
Listing 4. 로그인 divs를 보여주고 숨기는 JavaScript
function readCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for(var i=0;i < ca.length;i++) {
var c = ca[i];
while (c.charAt(0)==' ') c = c.substring(1,c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
}
return null;
}
function handle_cached_user() {
var login_cookie = readCookie('login');
var logged_in = document.getElementById('logged_in');
var logged_out = document.getElementById('logged_out');
if(login_cookie == null) {
logged_in.style.display = 'none';
logged_out.style.display = 'block';
} else {
logged_out.style.display = 'none';
logged_in.style.display = 'block';
}
}
|
첫 번째 함수는 JavaScript에서 쿠키의 값을 읽고 두 번째 함수는 DOM을 다룬다. Prototype 라이브러리를 사용함으로써 이 코드를 단순화 할 수 있지만, 기본 DOM 룩업을 포함시키도록 하겠다. 마지막 단계는 페이지가 로딩될 때 JavaScript 함수를 호출하는 단계이다. 다음 코드를 레이아웃에 추가한다.
Listing 5. 페이지 로딩 시 JavaScript 함수 호출하기
<script type="text/javascript">
window.onload = function() {
handle_cached_user();
<%= render_nifty_corners_javascript %>
<%= yield :javascript_window_onload %>
}
</script>
|
매우 단순한 JavaScript이다. 페이지가 로딩될 때 handle_cached_user 함수를 로딩하면, 알맞은 비트를 보여주거나 숨긴다. 이제, 다음을 필자의 컨트롤러에 추가함으로써 페이지 캐싱을 실행할 수 있다.
모든 것이 완벽하게 작동한다. 하지만, 페이지를 종료시키고 싶다면 여전히 주기적으로 캐시에서 프론트 페이지를 삭제해야 한다. 이렇게 하려면, public/index.html을 주기적으로 삭제한다. Hide-and-seek 방식은 여러 사용자 분류를 갖고 있는 페이지에 잘 맞지만, 그림 2에서 보았던 일부 사용자에게는 맞지 않는다. 여기에는 Hide-and-seek과 Show-and-tell 기술을 결합하여 사용할 것이다.
Show-and-tell 구현하기
그림 2를 다른 측면에서 보자. 필자는 Hide-and-seek을 사용하여 사용자가 로그인 했는지 여부에 따라서 이 파셜의 올바른 버전을 선택한 다음, Show-and-tell 기술을 사용하여 Listing 3에서 작성했던 쿠키의 콘텐트에 기반하여 페이지의 동적인 부분들을 채울 것이다. Show-and-tell의 경우, 필자는 특히 한 페이지의 엘리먼트들을 수정하여 한 명의 사용자에게 순응하도록 할 것이다.
먼저, 각 파셜을 렌더링 하는 정적인 콘텐트가 있다. 하나는 로그아웃을 한 사용자용이고, 하나는 로그인을 한 사용자용이다. 여기에서는 사용자가 로그아웃을 한 것으로 간주할 것이기 때문에 display: none 스타일을 붙여서 logged_in div를 숨길 것이다. 나중에 JavaScript를 사용하여 이들을 보여주거나 숨길 수 있다. 여기에서도 같은 이름, logged_in과 logged_out을 사용하여 각 div를 구분하기 때문에, 홈 페이지용으로 작성했던 JavaScript를 수정할 필요가 없다.
Listing 6. 로그인 및 로그아웃 파셜 렌더링 하기
<div class="boxRight sideColumnColor">
<div id='logged_in'>
<%= render :partial => 'common/logged_in' style="display: none; %>
</div>
<div id='logged_out'>
<%= render :partial => 'common/logged_out' %>
</div>
</div>
|
다음에는, logged_in 파셜용 콘텐트가 있다. 동적 콘텐트를 포함하고 있는 각 HTML 컴포넌트는 아이디를 갖고 있기 때문에 JavaScript를 사용하여 이를 찾을 수 있고 나중에 대체할 수 있다.
Listing 7. logged_in 파셜 보여주기
<div id='logged_in' style="display: none;">
<%= link_to %(<span class="mainBodyDark">Hi, </span>) +
%(<span class="textLarge mainBodyDark"><b id='bold_link'>) + "my_login" +
%(</b></span>), {:controller => 'profiles', :action => 'show', :id => 'my_login'},
{:id => 'profile_link'} %>
<br/>
<div id='picture_and_link'>
<a href="http://member/my_login" id='link_for_member_thumbnail'>
<img id='member_thumbnail'
alt="Def_member_thumbnail"
src="/images/default/def_member_thumbnail.gif" /></a>
</div>
<div id="not_mine">Not my_login?</div>
<br/>
<%= image_button "logout", :controller => "members", :action => "logout" %>
|
여러분이 Rails를 잘 알고 있다면, 여기에서 몇 가지 커스텀 헬퍼 함수를 인식했을 것이다. 각 페이지 로드를 JavaScript로 대체해야 할 동적인 콘텐트 조각 네 가지가 보인다. (로그인 세 곳과 멤버 이미지 한 곳). JavaScript 코드는 handle_cached_user 함수의 변형과, 동적인 사용자용 페이지 업데이트를 핸들하는 새로운 메소드로 구성된다. 이 글을 위해 코드를 약간 단순화 했다. 다음 함수를 application.js 파일에 추가하라.
Listing 8. 사용자 파셜의 엘리먼트 대체하기
function handle_user_partial() {
var login_cookie = readCookie('login');
var image_cookie = readCookie('image');
var profileLink = document.getElementById('profile_link');
profileLink.href = '/member/' + login_cookie;
document.getElementById('bold_link').firstChild.nodeValue=login_cookie;
document.getElementById('not_mine').firstChild.nodeValue="Not " + login_cookie + "?";
document.getElementById('link_for_member_thumbnail').href="/member/" + login_cookie;
document.getElementById('member_thumbnail').src=image_cookie.replace(/%2[Ff]/g,"/");
document.getElementById('member_thumbnail').alt=login_cookie;
}
|
Listing 8에서, JavaScript 함수는 먼저 쿠키를 읽고, DOM 트리의 한 부분을 가져온다. profile_link라고 하는 현재 사용자 프로파일에 대한 링크이다. 그리고 나서, handle_user_partial은,
- my_login 대신에 로그인을 한 사용자 이름(login_cookie에 저장됨)으로 대체하여 사용자의 프로파일 페이지에 정확한 URL을 만든다.
- 로그인을 한 사용자의 이름을 로그인을 한 사용자를 단순화 하는 볼드체 텍스트를 포함하고 있는 DOM 엘리먼트에 삽입한다.
- 간단한 문장, "Not login?"을
login 파셜에 logout 캡션을 포함하고 있는 DOM 엘리먼트로 삽입한다.
- 멤버 이미지를 포함하고 있는 dom 엘리먼트를 찾고, 일반 이미지용 URL을 멤버 이미지용 URL로 대체한다. 이것은
image_cookie에 있다.
- 이미지가 나타나지 않을 경우에는, 이미지용
alt 태그를 login 이름으로 대체한다.
DOM을 검색할 때, 가끔은 DOM 엘리먼트로 직접 가야하고, 텍스트를 다루어야 하는 경우에는 그 엘리먼트의 특정 자식이 필요하다는 것을 알 것이다. 필자는 firstChild 함수를 사용하여 찾고자 하는 DOM 아이템의 첫 번째 자식 엘리먼트를 찾았다. Prototype 라이브러리는 더욱 친숙한 신택스로 DOM 엘리먼트를 더 쉽게 다루지만, 이 글에서는 설명하지 않겠다.
이미 모든 쿠키들을 만들었으므로, 마지막 단계는 기존 handle_cached_user 함수에서 JavaScript를 호출하는 것이다. 이 함수는 public/javascripts/application.js에 있다.
Listing 9. handle_user_partial 함수를 handle_cached_user에 추가하기
function handle_cached_user() {
var login_cookie = readCookie('login');
var logged_in = document.getElementById('logged_in');
var logged_out = document.getElementById('logged_out');
if(login_cookie == null) {
logged_in.style.display = 'none';
logged_out.style.display = 'block';
} else {
handle_user_partial();
logged_out.style.display = 'none';
logged_in.style.display = 'block';
}
}
|
else 조건 내에 있는 handle_cached_user의 추가 라인을 주목하라. 이 라인은 logged_in DOM 엘리먼트를 보이기 전에 적절한 대체를 만든다. 나머지는 이 글과 이전 글에서 보았던 페이지 캐싱 명령어를 사용하여 전체 페이지를 저장(cache)하면 된다.
결론
이러한 고급 기술들은 많은 기회를 제공한다. ChangingThePresent.org에서, 매우 단순한 시간 기반 스위퍼들을 사용하여 75% 이상의 페이지를 저장할 수 있었다. 보다 고급의 스위핑 기술을 사용함으로써, 90% 이상의 페이지 히트를 캐싱할 수 있다. 그 이상도 가능하다. 이를 더욱 적극적으로 활용하면, 전체 웹 요청 중 1%에서 3% 정도만 애플리케이션 서버를 히트하게 될 것이다.
단점도 유념하기 바란다. 시스템이 더 복잡해졌다. 더 복잡한 HTML 코드를 관리해야 하고, HTML과 JavaScript를 동기화 시켜야 한다. 하지만 장점은, 더 나은 성능을 얻고자 할 때 가장 단순하고 효과적인 캐싱 기술을 사용할 수 있다—는 점이다. ChangingThePresent.org로 가서 홈 페이지를 로딩해 보라. 그런 다음, 탑 레벨 메뉴를 로딩하라. 우리는 탑 레벨 메뉴 6개 중에서 4개를 캐시에 저장했다. 계정을 만들고, 각각 재 로딩하라. 어떤 페이지가 캐싱되었는지 알 수 있겠는가? 다음 글에서는, ActiveRecord 성능을 높이는 기술에 대해 설명하겠다.
참고자료 교육
제품 및 기술 얻기
필자소개  | 
|  | Bruce Tate는 베스트 셀러인 Jolt winner Better, Faster, Lighter Java의 저자이다. 최근에는 From Java to Ruby and Rails: Up and Running를 출간했다. IBM에서 13년 동안 근무했으며, 자바와 Ruby on Rails에 기반한 경량 개발 전략과 아키텍트를 전문적으로 다루는 RapidRed를 창립했다. 현재 Rails 개발자들과 함께 ChangingThePresent.org를 개발 중이다. |
기사에 대한 평가
|  |