React 사용 시 Facebook like button 동적 로딩 삽질

페이스북의 소셜 플러그인 중의 하나인 좋아요(Like) 버튼은 간단해 보이지만 어떤 특정 상황에서는 꽤 성가신 버튼 중의 하나입니다. 이번 글에서는 최근 Popit 첫페이지 개편 작업을 중에 겪었던 페이스북 좋아요 버튼 적용 삽질기를 공유하겠습니다. 참고로 필자는 자바스크립트(또는 리액트) 관련해서는 그냥 막 사용하는 수준이라 내부에서 구체적으로 어떻게 동작하는지에 대해서는 잘 알지 못하는 수준입니다[1].

구현해야 하는 화면

다음 화면은 새롭게 구현된 Popit의 첫화면입니다. 이 화면에는 보시는 것 처럼 모든 포스트에 대해 페이스북 좋아요 버튼이 있습니다. 그리고 각 라인의 우측에는 Next 버튼이 있어 이 버튼 클릭 시 ajax 호출로 다음 포스트를 가져와 화면에 표시합니다.

popit_v2_design

새로운 페이지는 http://www.popit.kr/v2 에서 확인할 수 있습니다[2].

리액트 컴포넌트 사용

새로 만드는 첫페이지의 구현은 서버는 Go 언어로, 화면은 React로 개발하였습니다. 처음에는 좋아요 버튼을 위해 구글에서 React 컴포넌트를 찾아서 사용하였는데 여러 가지 사용해보았지만 대부분의 컴포넌트는 다음 두가지 문제점이 있었습니다.

  • 일부 컴포넌트는 처음 화면 로딩시에는 정보가 잘 나타나지만 "Next 버튼" 클릭 시 조회되는 컨텐츠에 대해서는 좋아요 건수를 가져오지 못합니다.
  • 일부 컴포넌트는 처음 화면 로딩시, Next 버튼 클릭 시 모두 정보를 잘 가져오지만 이것 역시 문제가 있었습니다.
    • 페이지 로딩 시 좋아요 버튼이 나타나면서 이미 나타난 버튼도 다시 리로딩(사용자 입장에는 계속 깜빡깜빡되는 느낌) 되는 문제
    • Next 버튼 클릭 시 화면에 해당 라인에 있는(새로 가져온 컨텐츠)의 좋아요 버튼만 변경되는 것이 아니라 화면에 있는 모든 좋아요 버튼이 2 ~ 3번 정도 깜빡 거리는 문제

이 문제는 처음에는 그냥 가볍게 넘어 갔는데 Popit에 많은 글 지분을 가지고 계신 안영회님이 다음과 같이 친절한(?) 피드백을 주셔서 해결해 보기로 하였습니다.

popit_page_tony_question

다른 서비스는 어떻게 하고 있는지 보기 위해 먼저 현재 Popit 서비스의 메인 페이지를 보았습니다. 이 페이지는 워드프레스에서 제공하는 화면 그대로를 사용하고 있으며, 좋아요 버튼은 워드프레스 플러그인(AddToAny)를 사용하고 있습니다. 이 페이지도 두번째 문제인 깜빡거림의 문제를 가지고 있었습니다.

문제의 원인

자바스크립트에 대해 깊이 알지 못하는 필자에게 페이스북에서 제공하는 소셜 플러그인을 모두 이해하기에는 어려움이 있었습니다. 검색을 통해 여러 문서를 확인해보니 다음과 같은 가설을 세울 수 있었습니다.

  1. Next 버튼 클릭 시 데이터가 나타나지 않는 것은 페이스북 소셜 플러그인 자바스크립트가 화면 로딩 시 한번만 동작하고 이후 Next 버튼 클릭 시 리액트에 의해 다시 render 되는 경우 스크립트가 실행되지 않기 때문이다.
  2. 반면에 좋아요 버튼이 계속해서 깜빡되는 것은 스크립트가 전역으로 적용되어 다음 포스트의 좋아요 버튼이 표시될 때 이전에 표시된 좋아요 버튼도 다시 리로드 되기 때문이다.

이런 가설에서 문제를 해결하는 방법은 각 포스트의 좋아요 버튼과 스트립트가 각 영역에서만 적용되도록 하는 것이라고 생각하고 이를 기반으로 다시 검색을 해보니 다음 키워드가 발견되었습니다.

FB.XFBML.parse: This function parses and renders XFBML markup in a document on the fly. This could be used if you send XFBML from your server via ajax and want to render it client side. XFBML enables you to incorporate FBML into your websites and IFrame applications.

리액트 Facebook Like Button 컴포넌트 개발

위 FB.XFBXML.parse 함수를 이용하여 다음과 같이 사용하면 해당 영역(각 포스트의 영역)만 재로딩할 수 있습니다.

 FB.XFBML.parse(document.getElementById('foo')); ... <div class="psot"><div id="foo" class="fb-like"></div>

하지만 여기서 다시 리액트에서는 이런 형태로 사용하기가 애매하고, FB 자바스크립트 객체를 초기화한 이후에나 사용할 수 있습니다. 이런 문제를 해결하기 위해 다음 예제를 보고 아이디어를 얻어  직접 리액트 컴포넌트를 개발하였습니다.

이 컴포넌트의 기본 아이디어는  대략 다음과 같습니다.

  • Like Button 컴포넌트의 render에서 FB 객체의 초기화를 담당하는 컴포넌트(FBLoader)를 생성
  • 이 FBLoader에 콜백 함수(onFbLoad)를 등록하고
    • onFbLoad에서는 Like Button의 setState()를 호출하여 Like Button을 다시 render 하게 한다.
  • FBLoader 컴포넌트에서는 페이스북 소셜 플러그인 자바스크립트의 로딩하고 FB 객체를 초기화 한 다음, 등록된 콜백 함수(onFbLoad)를 호출한다.
  • 이렇게 되면 화면은 모두 렌더가 된 상태이고 여기서 좋아요 정보를 가져올 실제 포스트의 링크 정보가 변경되면 Like Button 컴포넌트의 componentWillUpdate() 가 호출되고 이때 FB 스크립트가 현재 렌더되어 있는 영역 중에 변경할 영역에 대해서만 변경을 하게 하기 위해 FB.XFBML.parse를 호출한다.

말로 표현하기에는 애매한 부분이 있는데 소스코드를 보면 대략 다음과 같은 형태입니다.

ShareButton.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
export default class ShareButton extends React.Component {
  constructor() {
    super();
    this.state = { fbLoaded: false };
  }
  componentWillUpdate(nextProps, nextState) {
    if (nextState.fbLoaded && !this.state.fbLoaded) {
      window.FB.XFBML.parse(this._scope);   // <-- 이 코드에서 해당 글의 좋아요 정보를 가져오는 스크립트 호출
    }
  }
  render() {
    return (
      <div>
        { /* FBLoader의 props에 FB 설정 상태 정보를 설정하는 callback 함수 등록 */ } 
        <FBLoader title={this.props.title} onFbLoad={() => this.setState({fbLoaded: true})}>
          { /* FB.XFBML.parse()의 파라미터로 전달될 re-render 할 컴포넌트에 대한 ref를 저장 */ }
          <div ref={(s) => this._scope = s}>
            <div className="fb-like"
                 data-href={this.props.url}
                 data-width="100"
                 data-layout="button_count"
                 data-action="like"
                 data-size="small"
                 data-show-faces="false"
                 data-colorscheme="dark"
                 data-share="true">
            </div>
          </div>
        </FBLoader>
      </div>
    );
  }
}

Post.js

1
2
3
4
5
6
7
8
9
10
11
12
13
export default class Post extends React.Component {
  render() {
    return (
      <div>
        <div>{post.title}</div>
        <div>
          { /* ShareButton 추가 */ }  
          <ShareButton url={post.link}/>
        </div>
      </div>   
    );   
  } 
}

FBLoader.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let fbLoaded = !!window.FB;
let fbCallbacks = [];
function onFbLoad() {
  fbLoaded = true;
}
export default class FBLoader extends React.Component {
  constructor(props) {
    super(props);
    if (fbLoaded) {
      // Injected and loaded
      if (props.onFbLoad) {
        props.onFbLoad();
      }
    } else {
      // Not yet injected
      fbInjected = true;
      let script = document.createElement('script');
      script.onload = onFbLoad;
      script.src = 'https://connect.facebook.net/es_US/sdk.js#xfbml=1&version=v2.5&appId=131306400631298';
      document.body.appendChild(script);
    }
  }
  render() {
    return React.Children.only(this.props.children);
  }
}

위 코드를 만들면서 조금 애매한 부분이 있는데 리액트의 컴포넌트 객체를 생성하는 경우와 이미 생성되어 있는 객체의 상태만 변경하는 경우에 따라 조금 달라진다는 것입니다.

정확하게 리액트의 라이프 사이클과 FB 스크립트의 동작간의 관계를 확인해야 하겠지만 필자의 경우에는 글을 로딩하는 경우 Post 컴포넌트의 객체가 매번 새로 생성되는 형태로 구성했습니다. 이렇게 하면 ShareButton 객체도 매번 생성이 됩니다. 재사용하는 경우 componentWillUpdate에서 parse를 호출하는 대신 componentDidUpdate 에서 parse를 호출해야 로딩이 되었습니다.  이부분은 라이프 사이클과 마구 섞여 제대로 정리가 되지 않아 여기까지만 정리하도록 하겠습니다.

개선된 Popit 서비스 첫화면의 전체 소스 코드는 다음 github repos에서 확인할 수 있습니다.

글을 마치며

Popit 서비스가 2016.08에 오픈할 때에는 등록된 글이 많지 않아서 채널별, 저자별로 운영하는 것은 생각도 못했었습니다. 글이 300개 이상 넘어가면서 부터 과거 글에 대한 노출에 대해 고민하기 시작했는데 여유 시간이 많지 않아 지금에서야 이렇게라도 개편을 진행해 보았습니다. 지금의 개편안도 부족한 시간, 부족한 지식으로 진행하게되어 만족하지는 않지만 저자 분들의 모든 글 하나하나가 의미 있기에 첫페이지 노출 시킬 수 있다는 것에 만족하려고 합니다. 문제 있는 몇몇 부분을 정리해서 곧 정식 오픈하고, 상세 조회 페이지도 작업을 진행하도록 하겠습니다.

[1]: 글을 보시고 부족한 부분이나 잘못된 부분이 있으면 피드백 주시면 바로 반영하겠습니다.

[2]: 현재 베타 서비스 중으로 정식 오픈되면 이 URL은 변경될 예정입니다.


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.