Outsider's Dev Story: JavaScript/Framework 카테고리 글 목록https://blog.outsider.ne.kr/Stay Hungry. Stay Foolish. Don't Be Satisfied.2024-03-15T15:14:01+09:00Textcube 1.10.7 : Tempo primo정적 사이트 생성기 GatsbyOutsiderhttps://blog.outsider.ne.kr/14262019-06-26T13:53:13+09:002019-02-08T22:35:45+09:00<p>최근 웹사이트를 정적사이트로 만들 일이 있어서 어떤 도구를 이용해서 만들면 좋을지 좀 찾아봤다. 여기서 정적사이트(static site)라고 말하는 것은 HTML, CSS, JavaScript로만 만들어진 사이트이다. 이런 용어의 정의가 아주 엄격한 것은 아니지만 보통 정적사이트는 사용하는 문서도 빌드할 때 모두 HTML로 만들고 가장 많이 사용하는 방식이 Markdown으로 글을 쓰고 정적사이트 생성기를 통해서 블로그로 만드는 것이다. 이렇게 사이트를 만들면 HTML, CSS, JavaScript, 이미지 등의 정적 파일만 CDN 등을 통해서 배포하면 별도의 서버를 운영할 필요도 없고 사용자가 많아져서 서버에 부하가 생길 걱정도 할 필요가 없다.(물론 정적 파일을 배포하는 서버 부하는 생각해야 한다.)</p>
<p>최근 흐름에 따라 정적사이트와 비슷한 맥락의 용어가 몇 가지 더 있다. 용어의 경계를 너무 명확히 할 필요는 없고 이마저도 시간이 지나면서 달라지기 마련이지만 내가 이해한 맥락에서 설명을 해보려고 한다. (잘못된 내용이 있으면 댓글로 의견 주시면 감사하겠습니다.)</p>
<p><a href="https://jamstack.org/">JAMstack</a>사이트를 보면 다음과 같이 정의되어 있다.</p>
<blockquote>
<p>Modern web development architecture based on client-side JavaScript, reusable APIs, and prebuilt Markup</p>
</blockquote>
<p>위 설명대로 <strong>JavaScript, API, Markup</strong>의 앞글자를 딴 스택이다.(CSS를 안 쓴다는 의미는 아니다.) 템플릿은 미리 만들어서 JavaScript와 함께 배포하고 API로 데이터를 불러오는 사이트를 말한다. 이렇게 보면 API를 전혀 안 쓰는(다고는 했지만, Google Analytics 등 일부 API는 쓸 수도 있다.) 정적사이트는 JAMstack이라고 할 수 있지만 모든 JAMstack이 정적사이트는 아니다.(데이터를 API로 불러오므로...)</p>
<p>또 하나는 Single Page Application을 의미하는 SPA라는 형태가 있다. SPA는 <a href="https://en.wikipedia.org/wiki/Single-page_application">위키피디아</a>에 따르면 다음과 같이 정의되어 있다.</p>
<blockquote>
<p>a web application or web site that interacts with the user by dynamically rewriting the current page rather than loading entire new pages from a server.</p>
</blockquote>
<p>React나 Vue가 인기를 얻으면서 더욱 많이 사용하는 방식인데 기본적으로 어떤 경로로 접속을 하던 항상 똑같은 HTML과 JavaScript를 받는다. 이후 이 JavaScript가 실행되면서 API로 데이터를 불러오고 마크업으로 화면을 보여주고 사용자가 이동하는 것에 맞추어서 Push state로 히스토리를 바꿔주면서 페이지를 보여주지만 실제로는 모두 프론트엔드에서 처리된다. 이런 면에서 보면 SPA도 JAMstack이라고 할 수 있지만 모든 JAMstack이 SPA는 아니다. JAMstack으로 구성했지만, Single Page로 구성하지 않을 수도 있으므로... 물론 현실에서는 SEO나 성능상의 이유로 SPA와 서버사이드 렌더링을 섞어서 쓰기도 한다.<br />
<br></p>
<h1>정적 사이트 생성기(Static Site Generator)</h1>
<p>설명이 길어졌는데 여기서 내가 만들고자 하는 것은 정적사이트였고 최신 정적사이트 생성기를 살펴봤다. 프론트엔드를 주로 하지 않은지 좀 되었기에 내가 아는 정적사이트 생성기는 <a href="https://jekyllrb.com/">Jekyll</a>에서 머물러 있다. 정적사이트의 시대를 만든 것이나 다름없는 대표 프로젝트고 <a href="https://pages.github.com/">GitHub Pages</a>도 기본은 Jekyll을 사용한다.</p>
<p>물론 Node.js 프로젝트에서 <a href="https://www.staticgen.com/metalsmith">Metalsmith</a>를 쓰고 있고, <a href="https://nodejs.github.io/nodejs-ko/">Node.js 한국 커뮤니티</a>에서는 <a href="https://hexo.io/">Hexo</a>를 쓰고 최근 <a href="https://mochajs.org/">mocha 웹사이트</a>는 <a href="https://www.11ty.io/">11ty</a>로 갈아탔지만 대부분 사용방법은 크게 다르지 않다. 문서는 Markdown으로 관리하면서 생성기에서 지원하는 몇 가지 문법으로 HTML 템플릿과 연결해서 빌드하면 정적사이트가 만들어진다.</p>
<p>정적 사이트 생성기가 하는 일이 뻔하고 기능의 차이도 크지 않기 때문에 대부분은 사용성이나 언어의 선호도 정도로 골라서 쓰게 되는 편이라고 생각하고 새로운 생성기가 나와도 크게 신경 쓰고 있지 않았다. 말했다시피 각 도구가 하는 일이 도긴개긴이고 러닝 커브도 높지 않아서 미리 찾아보거나 공부해 볼 필요성을 못 느끼고 있었다.<br />
<br></p>
<h1>Gatsby란?</h1>
<p>기술 트랜드는 어느 정도 보고 있었기에 정적 사이트 관련해서 <a href="https://netlify.com/">Netlify</a>와 <a href="https://www.gatsbyjs.org/">Gatsby</a>를 중심으로 인기를 끌고 있다는 것은 어느 정도 알고 있었다. 내 주변에 쓰는 사람이 없어서 정확히 각 도구/서비스의 기능을 모르고 있었다가 얼마 전 Netlify를 써보고는 반해버렸고(<a href="https://blog.outsider.ne.kr/1417">netlify로 정적 사이트 배포하기</a> 참고) 이번에는 Gatsby를 써보고 왜 사람들이 계속 Gatsby를 언급했는지 이해하게 되었다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/3340012696.gif" width="750" height="327" alt="Gatsby 웹사이트" title="" /></p>
<p>처음에는 이게 뭔가? 하고 보고 있었는데 동작하는 구조를 이해하고 나서는 최근 몇 년간 정적 사이트의 발전이 너무 커서 놀라울 정도였다. 그동안 정적 사이트에 대한 요구가 없었던 관계로 React, Vue등을 이용한 SPA 쪽에 더 관심이 있었지(실제로 내가 보는 프로젝트도 이쪽이 많았고...) 정적 사이트는 안 보고 있었는데 안 보는 사이에 (내 처지에서) 놀라울 정도의 발전이 있었다. Netlify도 사실상 이런 흐름을 타고 인기를 얻고 있는 거고(실제로도 잘 만들었지만) 정적사이트 생성기를 정리해 놓은 <a href="https://www.staticgen.com/">StaticGen</a>도 Netlify에서 운영하고 있다.</p>
<p>Gatsby를 한마디로 정의하기가 어렵다. 앞에서 정적사이트 생성기를 설명하면서 Gatsby까지 왔지만, Gatsby를 단순히 정적 사이트 생성기라고 말하기에는 Gatsby가 아깝다고 생각한다.</p>
<blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">What's your preferred description of Gatsby? (if other, post in replies)</p>— Gatsby (@gatsbyjs) <a href="https://twitter.com/gatsbyjs/status/1090650859619246080?ref_src=twsrc%5Etfw">January 30, 2019</a></blockquote>
<p>Gatsby가 <a href="https://twitter.com/gatsbyjs">트위터</a>에서 Gatsby를 어떻게 설명하면 좋을지 설문한 것을 보면 <strong>Website generator</strong>나 <strong>Website development tool</strong>가 우세하다. 한쪽으로 치우치지 않고 Gatsby 쪽도 저렇게 애매한 단어로만 설문한 걸 보면 한마디로 정의하기 힘들어하는 것으로 보인다.</p>
<p>Gatsby는 React를 사용하는데 웹사이트를 쉽게 제작할 수 있도록 편의 기능을 많이 제공하고 있다. React를 잘 알지는 못하지만, React의 사용방법을 크게 헤치지 않으면서도 쉽게 웹사이트를 만들 수 있도록 잘 감싸서 제공하고 있다. React를 잘 아는 사람은 어떻게 느낄지 모르겠는데 나한테는 꽤 재밌고 잘 만들어진 도구로 느껴졌고 정적사이트는 정적 사이트 나름의 한계가 있지만 웬만한 사이트는 Gatsby를 먼저 고려해도 괜찮겠다 싶을 정도였다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/9607203875.gif" width="750" height="674" alt="Gatsby 동작 방식" title="" /></p>
<p>위 그림을 <a href="https://www.gatsbyjs.org/">Gatsby 홈페이지</a>에서 가져온 그림으로 동작 방식을 설명하고 있다.</p>
<p>블로그뿐만 아니라 웹사이트를 만들려면 데이터가 필요하다. 이 데이터를 가져오는 곳은 Gatsby에서는 <strong>데이터소스</strong>라고 부르는데 여기서 데이터 소스는 <a href="https://wordpress.org/">Wordpress</a>같은 CMS 도구가 될 수도 있고 다른 정적 사이트 생성기처럼 Markdown 파일이 될 수도 있고 API 등을 통해서 다른 곳에서 가져올 수도 있다. 플러그인 시스템이 잘 되어 있어서 <a href="https://www.gatsbyjs.org/plugins/?=source">다양한 데이터소스</a>에서 데이터를 가져올 수 있다.</p>
<p>Gatsby는 기본적으로 <strong><a href="https://graphql.org/">GraphQL</a>을 사용해서 데이터소스에서 데이터를 가져온다</strong>. 플러그인을 이용해서 RESTful API에서 데이터를 가져올 수도 있지만 GraphQL이 Gatsby에 포함되어 있어서 훨씬 쉽게 쓸 수 있고 원하는 데이터를 선택해서 가져올 수 있는 GraphQL이 Gatsby의 개념에도 더 어울린다고 생각한다.</p>
<p>웹사이트는 React를 이용해서 만든다. 코드는 <strong>React를 이용</strong>해서 컴포넌트 작성하듯이 사용하지만, 공통 레이아웃을 관리하거나 페이지를 생성하거나 데이터소스와 컴포넌트를 연결하거나 하는 기능을 Gatsby에서 제공하고 있어서 쉽게 만들 수 있다.</p>
<p>이를 <strong>빌드하면 정적 사이트로 만들어 준다.</strong> 여기서 SPA라고 하지 않고 정적사이트로 만든다는 부분이 중요한데 CMS나 파일 등의 데이터 소스에서 GraphQL로 가져온 데이터를 빌드할 때 모두 가져와서 정적파일의 데이터로 포함한다. SPA라고 한다면 API로 가져오는 로직이 소스에 들어있고 사용자가 SPA 사이트를 실행할 때 API로 가져오게 되지만 Gatsby는 정적 사이트를 만들어주므로 <strong>빌드 시에 GraphQL로 데이터를 가져와서 빌드된 배포 파일에 포함 시킨다.</strong> 그러므로 사이트를 운영할 때 데이터소스로 이용한 API 서버나 파일은 제공하지 않아도 된다. <strong>이 부분이 다른 도구들과 Gatsby를 가장 다르게 만들어주는 특징이라고 생각한다.</strong></p>
<p>복잡한 사이트를 아직 안 만들어봤지만, 이 전체 흐름이 아주 깔끔하게 만들어져있다. 필요한 API를 이용하거나 정적 사이트에 필요한 기능이나 빌드 과정까지 기존에 없던 새로운 기술은 아니지만 아주 우아하게 잘 조합하고 감싸서 제공하고 있다. 몇 번 테스트로 만져본 정도지만 사용 경험이 꽤 만족스럽다. 자세한 기능은 <a href="https://www.gatsbyjs.org/features/">문서</a>에서 확인할 수 있다.<br />
<br></p>
<h1>Gatsby의 동작 예시</h1>
<p>(아직 나도 잘 모르는) 자세한 사용법은 다음 기회에 설명하고 간단한 보일러 플레이트 프로젝트로 Gatsby가 어떻게 동작하는지 이해해 보자.</p>
<p>최근 대부분의 JavaScript 프로젝트처럼 사용하기 쉽게 <a href="https://www.npmjs.com/package/gatsby-cli">gatsby-cli</a>를 제공하고 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ npx gatsby-cli new gatsby-example
npx: 211개의 패키지를 15.471초만에 설치했습니다.
info Creating new site from git: https://github.com/gatsbyjs/gatsby-starter-default.git
Cloning into 'gatsby-example'...
remote: Enumerating objects: 1, done.
remote: Counting objects: 100% (1/1), done.
remote: Total 1176 (delta 0), reused 1 (delta 0), pack-reused 1175
Receiving objects: 100% (1176/1176), 2.55 MiB | 2.43 MiB/s, done.
Resolving deltas: 100% (678/678), done.
success Created starter directory layout
info Installing packages...
yarn install v1.10.1
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
Done in 16.16s.
</code></pre>
<p><code>npx gatsby-cli new gatsby-example</code> 명령어로 기본 웹사이트를 생성했다. 참고로 <a href="https://www.npmjs.com/package/gatsby-cli">gatsby-cli</a>를 전역으로 설치(<code>-g</code>)하면 <code>gatsby</code> 명령어를 사용할 수 있지만, 전역 설치를 안 하기 위해 <a href="https://www.npmjs.com/package/npx">npx</a>를 이용해서 <code>gatsby-cli</code>를 바로 실행했다. 그래서 <code>npx gatsby-cli new gatsby-example</code>는 <code>gatsby new gatsby-example</code> 명령어와 같다.(이후에는 그냥 <code>gatsby</code> 명령어를 사용한다.)</p>
<p><code>gatsby new PROJECT_NAME STARTER_URL</code> 명령어는 새로운 Gatsby 프로젝트를 만드는 명령어다. 위에서는 <code>STARTER_URL</code>을 생략했으므로 기본값인 <a href="https://github.com/gatsbyjs/gatsby-starter-default">gatsby-starter-default</a>를 사용해서 사이트가 생성되었다. 커뮤니티가 만든 <a href="https://www.gatsbyjs.org/starters/">다양한 구성의 Starter가 있으므로</a> 원하는 구성에 맞춰서 선택하거나 사용방법을 알아보기 좋다.</p>
<p>이제 <code>gatsby-example</code> 폴더에서 <code>npm run develop</code> 명령어(실제로는 <code>gatsby develop</code>)를 실행하면 개발 모드로 서버를 실행할 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">$ npm run develop
DONE Compiled successfully in 3279ms 21:08:13
You can now view gatsby-starter-default in the browser.
http://localhost:8000/
View GraphiQL, an in-browser IDE, to explore your site's data and schema
http://localhost:8000/___graphql
Note that the development build is not optimized.
To create a production build, use gatsby build
ℹ 「wdm」:
ℹ 「wdm」: Compiled successfully.
</code></pre>
<p>기본 스타터의 디자인은 중요하지 않지만 <a href="http://localhost:8000/">http://localhost:8000/</a>로 접속하면 아래와 같은 간단한 데모 페이지를 볼 수 있다. 기본 스타터는 2페이지로 구성되어 있다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/8151873884.gif" width="500" height="657" alt="Gatsby 기본 스타터 웹페이지" title="" /></p>
<p>개발 모드에서는 GraphQL을 테스트하기 쉽게 <a href="https://github.com/graphql/graphiql">GraphiQL</a>이 포함되어 있다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/2328007222.gif" width="750" height="455" alt="GraphiQL 페이지" title="" /></p>
<p>소스 파일이 있는 <code>src</code> 디렉터리를 보면 React로 작성한 웹사이트를 만든 정적 파일들이 있다. 상세 소스는 <a href="https://github.com/gatsbyjs/gatsby-starter-default/tree/master/src">GitHub 저장소</a>에서 볼 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">src
├── components
│ ├── header.js
│ ├── image.js
│ ├── layout.css
│ ├── layout.js
│ └── seo.js
├── images
│ ├── gatsby-astronaut.png
│ └── gatsby-icon.png
└── pages
├── 404.js
├── index.js
└── page-2.js
</code></pre>
<p>이를 프로덕션용 정적 사이트를 빌드하기 위해 <code>npm run build</code> 명령어(실제로는 <code>gatsby build</code>)를 실행해 보자.</p>
<pre class="line-numbers"><code class="language-bash">npm run build
> gatsby-starter-default@0.1.0 build /Users/outsider/temp/gatsby/gatsby-example
> gatsby build
success open and validate gatsby-configs — 0.010 s
success load plugins — 0.153 s
success onPreInit — 0.584 s
success delete html and css files from previous builds — 0.009 s
success initialize cache — 0.010 s
success copy gatsby files — 0.045 s
success onPreBootstrap — 0.003 s
success source and transform nodes — 0.040 s
success building schema — 0.173 s
success createPages — 0.000 s
success createPagesStatefully — 0.024 s
success onPreExtractQueries — 0.003 s
success update schema — 0.082 s
success extract queries from components — 0.076 s
success run graphql queries — 0.066 s — 7/7 109.09 queries/second
success write out page data — 0.002 s
success write out redirect data — 0.000 s
⠂ onPostBootstrapdone generating icons for manifest
success onPostBootstrap — 0.104 s
info bootstrap finished - 3.851 s
success Building production JavaScript and CSS bundles — 5.356 s
success Building static HTML for pages — 0.464 s — 4/4 27.16 pages/second
info Done building in 9.673 sec
</code></pre>
<p>빌드 결과는 <code>public</code> 디렉터리에 생기는데 위의 <code>src</code>와 비교했을 때 파일도 많아지고 훨씬 복잡해진 것을 알 수 있다.</p>
<pre class="line-numbers"><code class="language-bash">public/
├── 0-3a2bd5c9a235c2c48f7f.js
├── 0-3a2bd5c9a235c2c48f7f.js.map
├── 404
│ └── index.html
├── 404.html
├── app-d044d5a4358eaf1a0738.js
├── app-d044d5a4358eaf1a0738.js.map
├── chunk-map.json
├── component---src-pages-404-js-1af22cf06cd461fb89b9.js
├── component---src-pages-404-js-1af22cf06cd461fb89b9.js.map
├── component---src-pages-404-js.a1e3226de724a2570055.css
├── component---src-pages-index-js-20cc3b8fc80f26983ef6.js
├── component---src-pages-index-js-20cc3b8fc80f26983ef6.js.map
├── component---src-pages-index-js.a1e3226de724a2570055.css
├── component---src-pages-page-2-js-b55efdbe24802f10f9a8.js
├── component---src-pages-page-2-js-b55efdbe24802f10f9a8.js.map
├── component---src-pages-page-2-js.a1e3226de724a2570055.css
├── icons
│ ├── icon-144x144.png
│ ├── icon-192x192.png
│ ├── icon-256x256.png
│ ├── icon-384x384.png
│ ├── icon-48x48.png
│ ├── icon-512x512.png
│ ├── icon-72x72.png
│ └── icon-96x96.png
├── index.html
├── manifest.webmanifest
├── page-2
│ └── index.html
├── pages-manifest-4666a1fb53ffe6f02316.js
├── pages-manifest-4666a1fb53ffe6f02316.js.map
├── static
│ ├── 6d91c86c0fde632ba4cd01062fd9ccfa
│ │ ├── 360b8
│ │ │ └── gatsby-astronaut.png
│ │ ├── 5c9de
│ │ │ └── gatsby-astronaut.png
│ │ ├── 7e26c
│ │ │ └── gatsby-astronaut.png
│ │ ├── 893cf
│ │ │ └── gatsby-astronaut.png
│ │ ├── a2541
│ │ │ └── gatsby-astronaut.png
│ │ └── fdadc
│ │ └── gatsby-astronaut.png
│ └── d
│ ├── 1025518380.json
│ ├── 164
│ │ └── path---404-html-516-62a-NZuapzHg3X9TaN1iIixfv1W23E.json
│ ├── 173
│ │ └── path---index-6a9-NZuapzHg3X9TaN1iIixfv1W23E.json
│ ├── 2011440971.json
│ ├── 44
│ │ └── path---404-22-d-bce-NZuapzHg3X9TaN1iIixfv1W23E.json
│ ├── 53
│ │ └── path---page-2-fbc-5a8-NZuapzHg3X9TaN1iIixfv1W23E.json
│ ├── 755544856.json
│ └── 920
│ └── path---dev-404-page-5-f-9-fab-NZuapzHg3X9TaN1iIixfv1W23E.json
├── webpack-runtime-586c75e86997621faa61.js
├── webpack-runtime-586c75e86997621faa61.js.map
└── webpack.stats.json
</code></pre>
<p>로컬에서 <code>8000</code> 포트로 웹서버를 띄워서 빌드한 파일을 제공한다고 했을 때 이 프로젝트에는 <code>http://localhost:8000/</code>와 <code>http://localhost:8000/page-2/</code> 두 개의 페이지가 있다. 아래는 이 두 페이지를 불러온 소스 파일이다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/9612958136.gif" width="750" height="161" alt="두 페이지의 소스 보기 비교" title="" /></p>
<p>작아서 잘 안 보이지만 각 페이지를 직접 URL로 접근하면 완전한 HTML 페이지가 다운로드 된다. 위 빌드 파일에서 <code>index.html</code>과 <code>page-2/index.html</code>이 다운로드 된 것이다.</p>
<p>정적 사이트를 빌드했으므로 당연한 얘기인데 위처럼 빌드 결과로 많은 파일이 나온 이유는 최적화를 해주기 때문이다. 다음 화면을 보면 루트 페이지를 불러온 다음에 <code>/page-2</code>로 가는 링크 위에 마우스 커서를 올리면 <code>page-2</code>에 필요한 파일을 미리 다운로드 받는데 이 파일도 전체 HTML 파일과 관련 파일이 아니라 필요한 부분만 분리된 파일이 다운로드 된다. 미리 다운로드를 다 했으므로 클릭했을 때는 당연히 아무 파일도 다운로드하지 않고 바로 <code>/page-2</code>를 불러온다.</p>
<p style="text-align: center;"><img src="//blog.outsider.ne.kr/attach/1/4697788821.gif" width="669" height="665" alt="사용자 삽입 이미지" title="" /></p>
<p>개인적으로 이렇게 특별히 신경 쓰지 않아도 쉽게 최적화해주는 부분이 Gatsby의 매력이라고 생각한다. <a href="https://www.gatsbyjs.org/plugins/">플러그인</a>이 많이 존재하기 때문에 필요한 기능을 추가하면 쉽게 오프라인을 지원하거나 PWA로 만드는 등의 작업을 쉽게 할 수 있다.</p>
<p>최근에 <a href="https://blog.outsider.ne.kr/1422">Vue.js를 사용해 보고 나서</a> Vue.js에 끌리고 있었는데 Gatsby 때문에 React를 해야겠다고 생각하는 중이다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1426?commentInput=true#entry1426WriteComment">댓글 쓰기</a></strong></p>Angular.js 서비스에서 컨트롤러에 이벤트 발생시키기Outsiderhttps://blog.outsider.ne.kr/11352015-03-31T22:47:02+09:002015-03-31T22:47:00+09:00<p>보통 <a href="https://angularjs.org/">AngularJS</a>를 사용할 때 View에서 사용하는 scope를 제어하기 위해서 <a href="https://docs.angularjs.org/guide/controller">Controller</a>를 작성하고 기능별로 모듈화한 <a href="https://docs.angularjs.org/guide/services">Service</a>를 작성한 다음에 Controller에 주입해서 사용한다.</p>
<pre class="line-numbers"><code class="language-javascript">angular.module('myServiceModule', [])
.controller('MyController', ['$scope','notify', function ($scope, notify) {
$scope.callNotify = function(msg) {
notify(msg);
};
}])
.factory('notify', ['$window', function(win) {
var msgs = [];
return function(msg) {
msgs.push(msg);
if (msgs.length == 3) {
win.alert(msgs.join("\n"));
msgs = [];
}
};
}]);
</code></pre>
<p>AngualrJS 공식 문서에 나와 있는 Service 예제처럼 <code>notify</code>라는 서비스를 만들면 이를 <code>MyController</code>에 주입하고 컨트롤러에서 이를 사용한다. 이 방식으로 체계적으로 모듈화할 수 있고 관심사를 쉽게 분리(Separation of Concerns)할 수 있다. 이렇다 보니 보통은 Service에서 제공하는 기능을 Controller에서 가져다가 사용하고 서로 간의 의존성은 끊는 것이 일반적이다.</p>
<p>그래서 Controller에서 Service로 호출하는 것이 일반적인데 최근에 <a href="http://socket.io/">Socket.IO</a>를 만지다 보니 Service에서 Controller쪽을 호출해야 하는 상황을 겪었다. 웹에선 보통 클라이언트에서 외부로 요청을 보내는 것이 자연스럽지만 최근에는 <a href="https://developer.mozilla.org/en-US/docs/WebSockets">WebSockets</a>이나 <a href="http://www.html5rocks.com/en/tutorials/eventsource/basics/">Server Sent</a>등도 존재하므로 서버에서 메시지를 받는 경우도 점점 늘어가고 있다.</p>
<p>이럴 때 서버와 통신을 담당하는 Service를 만들었다면 이를 통해서 메시지를 서버로 보낼 때는 상관없지만, 서버에서 메시지가 왔을 때는 Service에서 Controller 쪽으로 호출해야 하는 상황이 발생한다. 오랜만에 Angular를 만지고 있기도 하지만 이 상황을 겪는 순간 갑자기 코딩이 막혔다. Controller를 Service에 주입하는 건 말도 안 되고 Service에서 이벤트가 발생했을 때 Controller 쪽으로 호출하는 방법이 필요했다. 관련 내용을 찾아보니 <a href="http://stackoverflow.com/questions/14056874/how-to-emit-events-from-a-factory">Stackoverflow의 "how to emit events from a factory"</a>라는 글이 제일 정리가 잘 되어 있었다.</p>
<pre class="line-numbers"><code class="language-javascript">angular.module('myServiceModule', [])
.factory('serverService', ['$rootScope', function($rootScope) {
return {
messageFromServer: function() {
$rootScope.$broadcast('messageReceived', {text: 'something'});
}
}
}]);
</code></pre>
<p>Service에 <code>$scope</code>를 전달하면 특정 컨트롤러와 의존성을 가지므로 좋은 방법이 아니다. 대신 <code>$rootScope</code>를 주입해서 <code>$broadcast</code>로 전체 앱에 이벤트를 시키고 필요한 파라미터를 전달하면 된다.</p>
<pre class="line-numbers"><code class="language-javascript">angular.module('myServiceModule', [])
.controller('MyController', ['$scope', function ($scope) {
$scope.$on('messageReceived', function(event, msg) {
});
}]).
</code></pre>
<p>이 이벤트를 받아야 하는 컨트롤러에서는 자신의 <code>$scope</code>에서 <code>$on</code>으로 해당 이벤트를 받으면 서비스와 의존성을 갖지 않고도 서비스에서 이벤트를 받을 수 있다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1135?commentInput=true#entry1135WriteComment">댓글 쓰기</a></strong></p>angular-summernote v0.3.1 릴리즈Outsiderhttps://blog.outsider.ne.kr/11092014-12-25T18:15:30+09:002014-12-25T18:15:30+09:00<p><a href="http://blog.outsider.ne.kr/1090">v0.3.0</a> 릴리즈 이후에 그동안 쌓였던 이슈를 다 털어버려서 프로젝트를 건드리지 않고 있었다. 11월부터 이슈가 하나씩 추가되기 시작했지만 바로 건드리지는 못하고 있었는데 지난번에 발생했던 <a href="https://docs.angularjs.org/error/$parse/isecdom">error:isecdom Referencing a DOM node in Expression</a> 오류에 대한 <a href="https://github.com/summernote/angular-summernote/issues/25">이슈</a>가 다시 올라왔다. 처음에는 중복이슈인 줄 알았는데 재확인을 해보니 저번에 처리한 내용이 제대로 해결이 안된 거였다. 그냥 A 상황에서 발생하던 문제를 B 상황으로 바꿔놓았을 뿐...</p>
<p><code>upload</code>나 <code>change</code> 이벤트가 발생하면 Summernote가 <code>editable</code> DOM을 콜백으로 전달하는데 Directive에서 받아서 콜백으로 전달해 주면 angular expression에서 DOM을 참조하지 말라는 오류가 발생한다. 전에는 콜백에서만 관련된 줄 알고 <code>editable=""</code>속성으로 객체를 따로 받아서 전달했는데 Summernote가 넘겨주는 DOM을 사용하는 것 자체에서 문제가 발생한다. 그래서 직접 <code>editable</code> DOM을 셀렉트해서 전달하니까 괜찮아 졌다. <code>editable</code> DOM은 어차리 summernote당 하나뿐이니까 한번 참조해 놓고 계속 전달하면 되니까 오히려 이전보다 괜찮은 것 같다. 이번 수정으로 해결됐기를...</p>
<p>그 사이에 <a href="https://github.com/summernote/summernote/releases/tag/v0.6.0">summernote v0.6.0</a>이 나와서 최신 버전으로 업데이트하고 <a href="https://github.com/summernote/angular-summernote/releases/tag/0.3.1">v0.3.1</a>을 릴리즈 했다.</p>
<p>그리고 <a href="https://github.com/HackerWins">HackerWins님</a>의 개인 프로젝트에서 시작한 summernote가 이제는 <a href="https://github.com/summernote">Summernote Organization</a>을 만들어서 django, rails등의 플러그인을 모두 한꺼번에 관리하기로 했다. 그래서 기존 저장소를 summernote아래로 옮겼으므로 주소도 <code>https://github.com/outsideris/angular-summernote</code>에서 <a href="https://github.com/summernote/angular-summernote">https://github.com/summernote/angular-summernote</a>로 변경되었다.(자동 리다이렉트된다.) <a href="https://github.com/summernote/angular-summernote">angular-summernote</a>는 계속 내가 관리하겠지만, 이제는 summernote의 이름 아래에서 관리된다. 원래도 공부 겸 <a href="http://blog.outsider.ne.kr/1019">summernote의 인기에 업혀갈 겸</a> 만들었지만 summernote 전체 프로젝트에 대한 알림을 받기 시작하자 얼마나 많은 사람이 쓰고 있는지 새삼 깨닫고 있다. ㅎㅎ</p>
<p><strong><a href="https://blog.outsider.ne.kr/1109?commentInput=true#entry1109WriteComment">댓글 쓰기</a></strong></p>angular-summernote v0.3.0 릴리즈Outsiderhttps://blog.outsider.ne.kr/10902014-10-20T23:50:29+09:002014-10-20T23:50:29+09:00<p><a href="http://blog.outsider.ne.kr/1086">지난 릴리즈 후</a>에 당분간 건드리지 않으려고 했는데 <a href="http://angularjs.blogspot.kr/2014/10/angularjs-130-superluminal-nudge.html">AngularJS 1.3.0</a>이 나오면서 오류가 생기는 부분이 있어서 패치를 했다.(1.3.0이 이렇게 금방 나올 줄이야...)</p>
<p>일단 오류를 수정하기 전에 Karma 테스트를 정리해야 할 필요가 있었다. 테스트를 꽤 충실이 작성했는데 AngularJS의 새 버전이 나온 관계로 angular-summernote를 1.2.x와 1.3.x을 모두 테스트해야 했다. 새 버전을 쓰는 사람도 있지만, 기존의 1.2.x를 그대로 사용하는 사람도 당연히 있을 테니까... 어떻게 할까 고민하다가 <a href="https://github.com/angular/angular.js">Angular.js</a>의 Karma 설정을 참고해서 공통부분과 의존성 부분을 따로 분리해서 버전별로 의존성을 관리해서 테스트가 동작하도록 만들었다.</p>
<p>일단 하나로 되어 있던 <code>karma.conf.js</code>파일에서 공통 부분은 <code>karma-shared.conf.js</code>로 분리하고 <code>karma.conf.js</code>에서 <code>karma-shared.conf.js</code>를 불러온 뒤 의존성 파일만 추가로 지정하는 방식을 취했다. 같은 방식으로 <code>karma-angular-1-2.x.conf.js</code>를 만들어서 AngularJS 1.2.x에 대한 의존성을 갖게 만들자 Grunt 명령어만으로 두 가지 버전을 테스트할 수 있었다. 이제 1.3에 맞춰서 개발할 때마다 틈틈이 1.2.x도 한 번씩 확인해 보면 될 것 같다.(귀찮아서 아직 Travis CI에는 연결하지 않았다.)</p>
<p>이번에 발생한 <a href="https://github.com/outsideris/angular-summernote/issues/20">오류</a>는 <a href="https://code.angularjs.org/1.3.0/docs/error/$parse/isecdom">error:isecdom Referencing a DOM node in Expression</a>에 관련된 오류인데 보안 문제로 Angular 표현식에서 DOM을 반환하지 못하도록 변경된 부분 때문에 발생했다. <a href="http://hackerwins.github.io/summernote/features.html#callbacks">summernote의 콜백</a>과 angualr-summernote의 콜백을 연결해 주는 부분이 있는데 이때 summernote에 콜백을 실행하면서 DOM 객체는 인자로 전달하는 부분이 문제가 되었다.<br />
원인은 파악했지만 어떻게 수정해야 할지가 좀 어려웠다. summernote 콜백에서 DOM을 사용해야 하니 넘겨주기는 해야 하는데 어떤 식으로 전달해 주는 것이 좋은지 몰라서 여러 가지를 시도하다가 사용자가 바인딩할 수 있도록 추가하고 이 바인딩 객체에 넘겨주는 방식을 취했다. 전에는 DOM 객체(<code>$editable</code>)가 필요한 콜백을 사용하는 경우 인자로 받았지만 0.3.0부터는 <code>editable="bindingObject"</code>같은 식으로 지정해서 이 객체로 전달받게 된다. 구현하고 나서도 좋은 해결책인지 좀 고민되지만 일단 동작은 정상적으로 하므로 사용자가 피드백 주면 그때 다시 고민해 보기로.. ㅎ</p>
<p>수정내용 자체는 많지 않지만 AngularJS 1.3.0을 지원하는 버전이므로 <a href="https://github.com/outsideris/angular-summernote/releases/tag/0.3.0">v0.3.0</a>으로 간만에 마이너 버전을 올려서 릴리즈했다.</p>
<p><strong><a href="https://blog.outsider.ne.kr/1090?commentInput=true#entry1090WriteComment">댓글 쓰기</a></strong></p>Angular.js의 ngClass 사용방법Outsiderhttps://blog.outsider.ne.kr/10452015-11-23T23:00:55+09:002014-04-13T16:01:38+09:00<p>Angualr.js의 <a href="http://docs.angularjs.org/api/ng/directive/ngClass">ngClass</a>는 Angular로 HTML 요소에 CSS 클래스를 동적으로 줄 수 있는 디렉티브다. HTML에서 클래스를 바꿔주는 건 자주 있는 일이지만 왠지 <code>ngClass</code>의 문법은 잘 안 외워져서 쓸 때마다 헷갈린다.(문서가 좀 별로라)</p>
<h2>ngClass</h2>
<pre class="line-numbers"><code class="language-html"><p ng-class="{strike: deleted, bold: important, red: error}">Example</p>
</code></pre>
<p>기본적인 문법은 다음과 같다. ngClass에 객체를 전달하고 키가 class 명이고 값 부분이 class를 사용하지 말지에 대한 표현식이다. 그래서 위 코드는 <code>deleted</code>, <code>important</code>, <code>error</code>라는 변수가 <code>true</code>이면 각각에 해당하는 <code>strike</code>, <code>bold</code>, <code>red</code>라는 CSS가 추가된다. 표현식을 쓸 수 있으므로 위처럼 변수 대신 다음과 같이 사용할 수도 있다. 우측이 <code>true</code>/<code>false</code>로만 떨어지면 된다.</p>
<pre class="line-numbers"><code class="language-html">ng-class="{selected: $index === 1}"
</code></pre>
<p>Angular 표현식으로 사용하면 다양한 상황에 제어할 수 있지만, 다음과 값에 따라 class를 다르게 줘야 한다면 꽤 복잡해진다.</p>
<pre class="line-numbers"><code class="language-html">ng-class="{food: type == 'food', place: type == 'place', people: type == 'people'}"
</code></pre>
<p>이런 경우에는 다음과 같이 간단하게 적을 수 있다.</p>
<pre class="line-numbers"><code class="language-html">ng-class="type"
</code></pre>
<p>다음과 같이 배열로 전달해서 여러 클래스를 한꺼번에 적용할 수도 있다. 이렇게 작성하면 <code>type</code>변수에 할당된 문자열이 class 명으로 적용된다.</p>
<pre class="line-numbers"><code class="language-html">ng-class="[type1, type2, type3]"
</code></pre>
<p>좀 더 세밀하게 제어하고 싶다면 클래스 명을 반환하는 함수를 작성하면 된다.</p>
<pre class="line-numbers"><code class="language-html"><p ng-class="customClass('type2')">Map Syntax Example </p>
</code></pre>
<pre class="line-numbers"><code class="language-javascript">app.controller('MainController', function($scope) {
$scope.customClass = function (name) {
var className = '';
if (name === 'type1') {
className = 'custom1';
} else if (name === 'type2') {
className = 'custom2';
} else if (name === 'type3') {
className = 'custom3';
}
return className;
};
});
</code></pre>
<p>사실 이런 정도의 커스텀은 함수를 쓸 필요도 없이 다음과 같은 문법으로도 사용할 수 있다.</p>
<pre class="line-numbers"><code class="language-html"><p ng-class="{type1: 'custom1', type2: 'custom2', type3: 'custom3'}[type]">Map Syntax Example </p>
</code></pre>
<p>마지막에 대괄호로 묶어준 변수의 값과 일치하는 키에 해당하는 값이 클래스명으로 추가된다.(원래 이 문법이 어려워서 글을 쓰기 시작한건데 테스트가 잘 안되서 내용에서 뺐다가 댓글에서 Sangmin Yoon님의 글을 보고 무슨 실수를 했는지 깨달았다 ㅡㅡ;;)</p>
<p><strong><a href="https://blog.outsider.ne.kr/1045?commentInput=true#entry1045WriteComment">댓글 쓰기</a></strong></p>