Caution: You are browsing the legacy symfony 1.x part of this website.

symfony advent calendar day eight: AJAX interactions

1.0

지난 줄거리

지난 7일동안 askeet 어플리케이션의 기능이 많이 발전했습니다. 메인 페이지는 질문들 목록을 출력하고, 질문을 클릭하면 해당 질문의 상세한 내용과 그것에 대한 답변들을 출력합니다. 사용자들은 각자의 프로필 페이지를 가지고 있고, 각각의 페이지는 그것과 관계된 메뉴들을 출력하는 사이드바를 가지고 있습니다. 우리의 커뮤니티 기반의 FAQ 시스템이 올바른 방향으로 가고 있는 것 같습니다 (사용가능한 액션들의 목록을 어제 살펴보았습니다). 하지만 아직 사용자들이 데이터를 수정하는것은 불가능합니다.

오랜 기간동안 폼은 웹을 통한 데이터 조작의 근간이 되어왔습니다만, 오늘날에는 AJAX 기술들과 사용성을 고려한 여러 기술들 덕분에 어플리케이션의 형태가 바뀔수 있게 되었습니다. 그러한 변화가 askeet 에도 적용이 가능합니다. 이 튜토리얼은 askeet 에 어떻게 AJAX 기술을 사용할 수 있는지를 살펴볼 것입니다. 최종목표는 질문에 등록된 사용자가 질문에 대한 흥미도를 등록할 수 있게 하는 것입니다.

레이아웃에 상태표시자 추가하기

AJAX 환경의 웹사이트에서는, 비동기 요청이 처리되고 있는동안, 사용자는 자신의 요청이 접수되었고 곧 결과가 출력될 것이라는 안내를 받을 수 없습니다. 이것이 모든 AJAX 페이지들이 상태표시자를 출력할 수 있어야만 하는 이유입니다.

그러한 이유로 글로벌 layout.php<body> 첫 부분에 다음을 추가합니다.

<div id="indicator" style="display: none"></div>

비록 이 <div> 단락은 처음에는 숨겨진채 출력되지만, AJAX 요청이 처리중일때는 화면에 나타날 것입니다. 해당 단락의 내용은 비었지만 main.css 스타일시트 (askeet/web/css/ 디렉토리) 에서 그것의 형태와 내용을 정의합니다.

div#indicator
{
  position: absolute;
  width: 100px;
  height: 40px;
  left: 10px;
  top: 10px;
  z-index: 900;
  background: url(/legacy/images/indicator.gif) no-repeat 0 0;
}

상태표시자

흥미도 추가를 위한 AJAX 인터렉션 추가

하나의 AJAX 인터랙션은 호출 (링크, 버튼, 또는 요청을 구동시킬 수 있는 컨트롤들), 서버측 액션, 그리고 처리결과를 출력할 부분, 세가지로 구분됩니다.

호출

질문이 출력되는 부분을 돌아가보겠습니다. 4일째 를 떠올려보시면, 질문은 질문들 목록과 질문 상세내역 페이지에서 출력된다는 것을 기억하실 것입니다.

질문목록

때문에 우리는 질문 제목과 흥미도 부분을 _interested_user.php 조각 파일로 리팩토링하였습니다. 이 파일을 다시 여시고 흥미도를 추가할 수 잇는 링크를 달아보겠습니다.

<?php use_helper('User') ?>
 
<div class="interested_mark" id="mark_<?php echo $question->getId() ?>">
  <?php echo $question->getInterestedUsers() ?>
</div>
 
<?php echo link_to_user_interested($sf_user, $question) ?>

이 링크는 다른 페이지로 사용자를 이동시키는 역할만 하는 것이 아닙니다. 사실, 어떤 사용자가 한 질문에 대해 한 번 흥미도를 표시했다면, 그 사용자는 더 이상 같은 질문에 대해 흥미도를 추가할 수 없어야 합니다. 또한 만약 사용자가 로그인 하지 않았다면.. 흠, 그 경우는 나중에 다루도록 하겠습니다.

위 링크는 헬퍼 함수이기 때문에 askeet/apps/frontend/lib/helper/UserHelper.php 를 만들어주어야 합니다.

<?php
 
use_helper('Javascript');
 
function link_to_user_interested($user, $question)
{
  if ($user->isAuthenticated())
  {
    $interested = InterestPeer::retrieveByPk($question->getId(), $user->getSubscriberId());
    if ($interested)
    {
      // already interested
      return 'interested!';
    }
    else
    {
      // didn't declare interest yet
      return link_to_remote('interested?', array(
        'url'      => 'user/interested?id='.$question->getId(),
        'update'   => array('success' => 'block_'.$question->getId()),
        'loading'  => "Element.show('indicator')",
        'complete' => "Element.hide('indicator');".visual_effect('highlight', 'mark_'.$question->getId()),
      ));
    }
  }
  else
  {
    return link_to('interested?', 'user/login');
  }
}
 
?>

link_to_remote() 함수는 AJAX 인터렉션의 첫번째 부분인 '호출' 을 담당합니다. 이를 통해 사용자가 링크를 클릭했을때 어떠한 액션이 실행되어야 하는지 (여기서는 user/interested), 그리고 반환되는 결과가 페이지의 어느 부분에 저장되어야 하는지 (여기서는 id 가 block_XX 인 단락) 를 정의합니다. 덧붙여 두개의 prototype 자바스크립트 함수들이 이벤트 핸들러 (loadingcomplete) 로 추가되었습니다. prototype 라이브러리는 간단한 함수 호출로 화려한 효과들을 만들 수 있도록 도와주는 매우 편리한 자바스크립트 도구입니다. 유일한 단점이라면 문서화가 잘 안 되어 있다는 점이지만, 소스코드 자체는 상당히 직관적입니다.

이 기능은 HTML 코드 보다는 PHP 코드가 많이 필요하므로 우리는 조각파일대신 헬퍼를 사용하기로 하였습니다.

question/_list 조각파일에 id="block_<?php echo$question->getId() ?>" 를 추가하는 것을 잊지 마십시오.

<div class="interested_block" id="block_<?php echo $question->getId() ?>">
  <?php include_partial('interested_user', array('question' => $question)) ?>
</div>    

참고: 이 기능은 여러분이 sf 앨리어스 (alias) 를 웹서버에 제대로 설정해야만 동작합니다. 1일째 를 참고하시기 바랍니다.

결과 표시 부분

link_to_remote() 자바스크립터 헬퍼는 update 설정값으로 결과가 표시되는 부분을 지정합니다. 위의 경우에 user/interested 액션의 결과는 block_XX 부분에 표시될 것입니다. 만약 아직 잘 모르시겠다면, 조각파일과 템플릿을 합하여서 어떤 모양이 될지를 한 번 살펴보겠습니다.

...
<div class="interested_block" id="block_<?php echo $question->getId() ?>">
  <!-- between here -->
  <?php use_helper('User') ?>
  <div class="interested_mark" id="mark_<?php echo $question->getId() ?>">
    <?php echo $question->getInterestedUsers() ?>
  </div>
  <?php echo link_to_user_interested($sf_user, $question) ?>
  <!-- and there -->
</div>
...

결과가 표시될 부분은 두 개의 주석문 사이입니다. 액션이 실행된다면 이 부분이, 반환되는 결과로 대체될 것입니다.

두번째 아이디 부분 (mark_XX) 의 목적은 비주얼적인 것입니다. 사용자가 클릭 후, 액션이 증가된 흥미도 숫자를 반환하면, link_to_remote 헬퍼는 interested_mark <div> 부분을 효과를 줄것입니다.

서버 액션

AJAX 호출은 user/interested 액션으로 연결됩니다. 이 액션은 Intereste 테이블에 클릭된 질문과 클릭한 사용자에 대한 정보를 저장할 수 있어야 합니다. Symfony 에서는 아래와 같은 방법으로 할 수 있습니다.

public function executeInterested()
{
  $this->question = QuestionPeer::retrieveByPk($this->getRequestParameter('id'));
  $this->forward404Unless($this->question);
 
  $user = $this->getUser()->getSubscriber();
 
  $interest = new Interest();
  $interest->setQuestion($this->question);
  $interest->setUser($user);
  $interest->save();
}

우리는 이미 Interest 객체의 ->save() 메쏘드를 수정해서 해당 메쏘드가 호출될때 해당 질문의 interested_user 갯수를 자동으로 늘리도록 해 두었습니다. 따라서 이 액션이 수행된 이후에는 흥미도 관련 숫자가 마법처럼 늘어나있는 것을 보실 수 있을 것입니다.

그럼 interestedSuccess.php 템플릿에는 어떤 내용이 들어가야 할까요?

<?php include_partial('question/interested_user', array('question' => $question)) ?>

위 코드는 question 모듈의 _interested_user.php 조각파일을 출력할 것입니다. 맨 처음 조각파일을 작성했던 것에 대한 보상이군요.

해당 템플릿은 글로벌 레이아웃이 표시되면 안 되기 때문에 modules/user/config/view.yml 파일을 수정합니다.

interestedSuccess:
  has_layout: off

테스트

AJAX 흥미도 버튼 개발이 끝났습니다. 로그인을 하신 후 질문 목록을 표시하고 'interested?' 링크를 클릭하셔서 테스트를 하실 수 있습니다. 요청이 진행되는 동안 상태표시자가 나타날 것입니다. 이 후 서버가 응답을 보내오면, 증가된 숫자를 보실 수 있을 것입니다. 맨 처음 'interested?' 였던 링크가 링크가 아닌 일반 텍스트인 'interested!' 로 바뀐것을 확인하십시오.

ajax

AJAX 헬퍼들에 대한 좀 더 많은 예제를 보시고 싶으시다면, 드래그앤드롭 쇼핑카드 튜토리얼 이나 스크린캐스트, 또는 온라인 문서중 관련된 부분 을 참조하시기 바랍니다.

'sign-in' 폼 추가하기

질문의 흥미도를 증가시키기 위해서는 로그인된 사용자여야만 합니다. 즉, 로그인 하지 않은 사용자가 'interested?' 링크를 클릭한다면, 로그인 페이지가 먼저 표시되어야만 할 것입니다.

잠깐만요. 사용자를 로그인 페이지로 이동시킨다면, 해당 사용자가 흥미도를 추가하고 싶은 질문은 페이지에서 사라져버릴 것입니다. 페이지에 로그인 폼을 동적으로 보이게 할 수 있다면 더 효과적일 것입니다. 이를 해보도록 하겠습니다.

레이아웃에 숨겨진 로그인 폼 추가하기

글로벌 레이아웃 (askeet/apps/frontend/templates/layout.php) 파일을 열고, headercontent 블록 (div) 사이에 다음을 추가합니다.

<?php use_helper('Javascript') ?>
 
<div id="login" style="display: none">
  <h2>Please sign-in first</h2>
 
  <?php echo link_to_function('cancel', visual_effect('blind_up', 'login', array('duration' => 0.5))) ?>
 
  <?php echo form_tag('user/login', 'id=loginform') ?>
    nickname: <?php echo input_tag('nickname') ?><br />
    password: <?php echo input_password_tag('password') ?><br />
    <?php echo input_hidden_tag('referer', $sf_params->get('referer') ? $sf_params->get('referer') : $sf_request->getUri()) ?>
    <?php echo submit_tag('login') ?>
  </form>
</div>

이 폼은 기본적으로는 숨겨져 있습니다. 'hidden' 필드인 referer 에는, 만약 요청값들 중에 referer 값이 있다면, 해당 값이, 그렇지 않다면 현재 URI 가 저장될 것입니다.

로그인 되지 않은 사용자가 흥미도 버튼을 눌렀을때 폼을 보이게 하기

전에 작성한 User 헬퍼가 생각나십니까? 이제 로그인 되지 않은 사용자의 경우를 다뤄보도록 하겠습니다. askeet/lib/helper/UserHelper.php 파일을 여시고 다음 코드를

return link_to('interested?', 'user/login');

아래와 같이 수정합니다.

return link_to_function('interested?', visual_effect('blind_down', 'login', array('duration' => 0.5)));

로그인 되지 않은 사용자가 'interested?' 링크를 클릭한다면 아이디가 login 인 개체 (우리가 레이아웃에 방금 추가한 폼) 가 프로토타입의 자바스크립트 이펙트 (blind_down) 과 함께 보여질 것입니다.

로그인 하기

user/login 액션은 5일째에 작성되었고, 6일째에는 리팩토링도 하였습니다. 또 수정해야 하나요?

public function executeLogin()
{
  if ($this->getRequest()->getMethod() != sfRequest::POST)
  {
    // display the form
    $this->getRequest()->getParameterHolder()->set('referer', $this->getRequest()->getReferer());
 
    return sfView::SUCCESS;
  }
  else
  {
    // handle the form submission
    // redirect to last page
    return $this->redirect($this->getRequestParameter('referer', '@homepage'));
  }
}

결과적으로는, 아닙니다. 해당 액션은 수정없이도 잘 작동하고, 로그인 후 사용자가 링크를 클릭했던 곳으로 되돌아가도록 합니다.

AJAX 기능을 테스트 해보시기 바랍니다. 로그인되지 않은 사용자는 페이지를 떠나지 않고도 로그인 폼을 볼 수 있을 것입니다. 사용자 이름과 비밀번호가 제대로 입력되었다면, 해당 페이지는 다시 호출될 것이고 이제 사용자가 'interested?' 링크를 클릭할 수 있을 것입니다.

로그인 폼

참고: 이런 AJAX 인터렉션에서는, 서버 액션이 사용하는 템플릿은 단지 간단한 include_partial 인 경우가 많습니다. 이는 대부분 처음에 표시되는 부분이 AJAX 액션에 의해 업데이트 되는 경우가 많이 때문입니다.

내일 이 시간에

AJAX 인터렉션을 디자인 하는 데 있어서 가장 어려운 부분은 호출자, 서버 액션, 그리고 결과 표시부분을 적절히 정의하는 것입니다. 일단 그들이 정의된 후에는, symfony 는 헬퍼들을 동원해서 나머지들을 해결해 줍니다. 보다 정확히 이해하기 위해서, 우리가 동일한 방식으로 질문에 대한 답변들의 관계도를 구축한 것을 살펴보시기 바랍니다. AJAX 액션은 user/vote, 템플릿 _answer.php 는 두 부분으로 쪼개졌고 (_user_vote.php 가 사용됩니다), link_to_user_relevancy_up()link_to_user_relevancy_down()User 헬퍼에 새로 생성되었습니다. User 모듈에도 vote 액션과 voteSuccess.php 템플릿이 새로 생겼습니다. 이들 템플릿에 대해 레이아웃을 off 시키는 것을 잊지 마십시오.

Askeet 이 이제 웹 2.0 어플리케이션처럼 보이기 시작하는 것 같습니다. 하지만 이제 시작일 뿐입니다. 며칠 안에 좀 더 많은 AJAX 인터렉션들을 추가하도록 하겠습니다. 내일은 symfony 가 사용하는 일반적인 MVC 테크닉들에 대해 살펴보고, 외부 라이브러리들을 정의해보겠습니다.

오늘 튜토리얼을 따라하는 와중에 문제가 생기신다면, 오늘의 소스들을 askeet SVN 저장소 에서 release_day_8 태그를 사용해서 다운로드 받으실 수 있습니다. 만약 질문이 없으시다면 askeet forum 에 오셔서 다른 사람들의 질문들에 대답해 보십시오.