Skip to main content

109 posts tagged with "javascript"

View All Tags

· 2 min read

문서가 있는데 생각보다 사용하기가 어렵다. 하나하나 시작해보자.

인증 토큰 발급

Cridentials page에서 Create credentials 를 눌러 OAuth Client ID 를 발급받는다.

예제

받은 Client ID 를 메타태그에 넣어준다.

example
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello Analytics Reporting API V4</title>
<meta name="google-signin-client_id" content="OAuth 2.0 Client ID">
<meta name="google-signin-scope" content="https://www.googleapis.com/auth/analytics.readonly">
</head>
<body>
<h1>Analytics Reporting API V4</h1>
<p>
<div class="g-signin2" data-onsuccess="queryReports"></div>
</p>

<!-- The API response will be printed here. -->
<textarea cols="80" rows="20" id="query-output"></textarea>

<script>
// Replace with your view ID.
var VIEW_ID = '136416454';

// Query the API and print the results to the page.
function queryReports() {
gapi.client.request({
path: '/v4/reports:batchGet',
root: 'https://analyticsreporting.googleapis.com/',
method: 'POST',
body: {
reportRequests: [
{
viewId: VIEW_ID,
dateRanges: [
{
startDate: '7daysAgo',
endDate: '6daysAgo'
}
],
metrics: [
{
expression: 'ga:sessions'
}
]
}
]
}
}).then(function (response) {
var formattedJson = JSON.stringify(response.result, null, 2);
document.getElementById('query-output').value = formattedJson;
}, console.error.bind(console));
}
</script>
<script src="https://apis.google.com/js/client:platform.js"></script>
</body>
</html>

파라미터 확인

문서의 내용을 확인하기 보다 Query Explorer에서 테스트 후에 그 값들을 Body 로 옮겨 적는 게 확인하기 쉽다.

· 4 min read

페이지나 리소스가 있는지 체크하는 방법은 서버사이드에선 엄청 간단하다. 단 두 가지 기능만 있으면 해결된다. 심지어 certinfo 값 안에선 인증서 만료일까지 확인할 수 있다.

  • HEAD 메소드
  • CURL

하지만 수 백개의 리소스를 동시에 체크해야할 경우는 어떨까? 리소스가 있는 서버에선 DDoS 공격으로 오인할 수 있고, 심지어 내 서버가 차단될 가능성도 있다.

클라이언트에서 리소스를 확인할 수 있는 방법이 있을까?

삽질

ajax

  • 당연하지만 크로스도메인 XHR 은 CORS 가 없는 이상 막힌다.
  • HEAD 메소드도 똑같다.
  • fetch 도 똑같다.

script

  • script tag 를 DOM 에 렌더링하는 것이므로 XSS 공격이 가능하다.
  • 스크립트 태그에 한해서 onLoad 와 onError 로 체크가 가능하다.

image

  • 이미지 태그에 한해서 onLoad 와 onError 로 체크가 가능하다.
  • css 일 경우 onLoad 와 onError 로 체크가 가능하다.
  • DOM 에 렌더링하는 것이므로 UI가 틀어질 수 있다.
  • html 등의 페이지도 체크가 가능하지만 firefox 등의 브라우저에서 일관성이 없다.
  • IE와 Edge 브라우저에서 없는 css 인 경우에도 onError 이벤트가 발생하지 않았다.

마지막 이슈는 크리티컬했는데, 다행히 에러를 만들어 낼 수 있었다. cssRules 는 스타일시트가 없을 경우 접근할 수 없는 내부 값이기에 에러를 던진다.

link.onload = (loadEvent) => {
// ie, edge 체크
const isIE = /MSIE|Trident|Edge/i.test(navigator.userAgent);

// sheet 는 현재 로드 된 스타일시트 엘레먼트
if (isIE && loadEvent.target.sheet) {
try {
// 강제로 시트 내의 cssRuls 값에 접근한다.
let temp = loadEvent.target.sheet.cssRules;
} catch (e) {
// onError 와 같은 이벤트 처리
}
}
};

video

  • 없는 영상에 대해 onError 가 동작하지 않는다.

기타

  • embed 나 iframe 은 X-Frame-Options 헤더에 차단되거나 XSS 공격이 가능하다.
  • onError 이벤트의 일관성이 없다.

해결

삽질의 결과로 js (script), css (link), image (img) 에 한해서 리소스 체크가 가능한 걸 확인했다. 하지만 js 와 css 의 렌더링으로 인해 페이지가 틀어지는 걸 어떻게 방지할 수 있을까?

불현듯 샌드박스란 단어가 떠올랐다.

sandbox

보이지 않는 샌드박스 프레임을 만들고, 거기에서 위험한 일을 하면 된다.

<iframe id="sandbox" src="about:blank" style="display:none;" />

만들고

$sandbox.contentDocument.write(`
<html>
<head>
<script>
여기에 프레임간 메세징과 리소스별 onLoad, onError 체크 로직을 넣는다.
</script>
</head>
</html>
`);

넣고

메인 프레임에선 샌드박스 메소드를 postMessage 하면 된다.

· 3 min read

아래 데이터로 php 와 js 의 다른 점을 확인해보자. id 가 3 이상인 id 만 추출하고 싶었다.

users.json
[
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz"
},
{
"id": 2,
"name": "Ervin Howell",
"username": "Antonette",
"email": "Shanna@melissa.tv"
},
{
"id": 3,
"name": "Clementine Bauch",
"username": "Samantha",
"email": "Nathan@yesenia.net"
},
{
"id": 4,
"name": "Patricia Lebsack",
"username": "Karianne",
"email": "Julianne.OConner@kory.org"
},
{
"id": 5,
"name": "Chelsey Dietrich",
"username": "Kamren",
"email": "Lucio_Hettinger@annie.ca"
}
]

AS-IS

JS

머리 속으로 돌려본 원래 느낌은 이랬다.

let userIdxs = users.map((user) => {
if (user.id >= 3) {
return user;
}
});

// userIdxs [ null, null, 3, 4, 5 ]

userIdxs = userIdxs.filter(Boolean);

// userIdxs [ 3, 4, 5 ]

php

생각 없이 짜면 array_map 을 먼저 사용할 수 있다.

$userIdxs = array_map(function ($user) {
if ($user['id'] >= 3) {
return $user['id'];
}
}, $users);

// userIdxs [ null, null, 3, 4, 5 ]
// 참담한 결과가 나왔다.

$userIdxs = array_filter($userIdxs);

// 필터를 먹여도 id: 3의 인덱스는 2이다.
// 이걸 해결하려면 array_values 를 한번 더 사용한다.
$userIdxs = array_values($userIdxs);
// userIdxs [ 3, 4, 5 ]

array_values(array_filter(array_map())) 과 같이 호출할 수 있긴 하다.

너무 지저분했다.

TO-BE

조금만 생각해도 FP 의 개념에 어긋남을 느낄 수 있다. 범위를 줄이고 나서 해당 값을 추출하는 게 맞다.

JS2

const userIdxs = users.filter((user) => user.id >= 3).map((user) => user.id);

// userIdxs [ 3, 4, 5 ]

php2

array_values 를 쓰지 않고도 깔끔한 코딩이 가능하다.

$userIdxs = array_filter($users, function ($user) {
return $user['id'] >= 3;
});

$userIdxs = array_map(function ($user) {
return $user['id'];
}, $userIdxs);

// userIdxs [ 3, 4, 5 ]

lara

물론 더 멋진 방법이 있다.

$userIdxs = collect($users)
->filter(function ($user) {
return $user['id'] >= 3;
})
->map(function ($user) {
return $user['id'];
})
->all();

// userIdxs [ 3, 4, 5 ]

여담

php, js, java, python 을 넘나들다보니 사용하는 언어의 흐름에 대한 개념이 1/n 로 줄어드는 것 같다.

· 2 min read

블로그에 붙힐만한 댓글 라이브러리로는 Disqus, Commento, livere 등이 있지만 깃헙 페이지라 Gitment를 사용하고 싶었다.

사전 준비

Github > Settings > Developer settings > OAuth Apps 메뉴로 들어가 새로운 OAuth App 을 만들어준다.

{%img https://i.imgur.com/76Q1E1K.png %}

Client ID 와 Client Secret 을 저장해 놓고 Authorization callback URL 은 Homepage URL 과 같은 주소를 입력한다.

# Application name
GracefulLight

# Homepage URL
https://gracefullight.github.io

# Application description
GracefulLights Blog

# Authorization callback URL
https://gracefullight.github.io

소스 추가

원하는 페이지에 소스를 추가한다.

<script src="https://cdn.jsdelivr.net/npm/bluebird@3.5.1/js/browser/bluebird.core.min.js"></script>
<section class="comments" id="comments">
<div id="gitment_thread"></div>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/gitment@0.0.3/style/default.css"
/>
<script src="https://cdn.jsdelivr.net/npm/gitment@0.0.3/dist/gitment.browser.js"></script>
<script>
var gitment = new Gitment({
id: "Gitment 를 구분할 아이디 (ex: 페이지 제목 또는 포스팅 일시)",
owner: "github 아이디 (ex: gracefullight)",
repo: "repository 명 (ex: gracefullight.github.io)",
oauth: {
client_id: "위에서 발급 받은 client_id",
client_secret: "위에서 발급 받은 client_secret",
},
});
gitment.render("gitment_thread");
</script>
</section>

gitment.min.js 파일은 없으므로, 직접 minify 해서 사용하면 된다. bluebird (promise) core 를 추가한 이유는, IE 에서 gitment 를 지원해야하기 때문이다.

옵션 관리

위 4개 옵션 외에 추가로 옵션을 더 줄 수 있다. desc 와 labels 정도가 추가되면 좋을 것 같다.

댓글 쓰기

깃허브 아이디로 로그인한 뒤 Initialize Comments 를 누르고 댓글을 작성하면 된다.

· 3 min read

Nuxt Project 에 Particles.js 를 붙히고 싶었다. 어떻게하면 쉽게 붙힐 수 있을까 하다가 멀리 돌아오게 된 삽질기다.

시작

vue-plugin 사용

공식 문서 를 봤었고 Vue Plugin일 경우 너무나 쉽게 추가가 가능한 것처럼 보였다. 얼른 vue-particles 를 설치하고 플러그인을 만들어 등록했다.

만들고

vue-particles.js
import Vue from "vue";
import VueParticles from "vue-particles";

Vue.use(VueParticles);

등록했다.

nuxt.config.js
module.exports = {
plugins: ["~/plugins/vue-particles"],
};

그런데 화면에 Particle이 보이지 않는다.

no-ssr

구글링을 하니, 플러그인에 no-ssr 옵션을 주면 해결이 된다고 한다.

nuxt.config.js
module.exports = {
plugins: [
{
src: "~/plugins/vue-particles",
ssr: false,
},
],
};
Particles.vue
<template>
<no-ssr>
<vue-particles />
</no-ssr>
</template>

대충 이런식으로 코딩했더니 화면에서 볼 수 있게 되었다.

그런데... IE11에서는 스크립트 오류가 발생하기 시작했다.

IE 오류

vue-particles 자체에 const 구문을 사용하고 있기 때문에, no-ssr 옵션을 준다면 번들링 시 로직을 건너 뛰기에, 크롬에서는 실행이 되지만 IE에서는 실행되지 않는 치명적인 오류가 발생했다.

그래서 다른 방법을 시도해봤다.

script 삽입하기

nuxt.config.js 에서 head 태그를 이용하면 스크립트를 추가할 수 있고, window.particlesJS 처럼 전역 변수로 참조하면 될 줄 알았다.

하지만 번들링 시 window 객체가 없어 aframe 벤더가 필요하다고 오류가 발생했다. 쓸데 없는 리소스를 추가해야되니 여기서 멈추었다.

해결

process.browser

window-document-undefined 문서에 따르면 이런 참조 문제를 해결할 수 있다고 한다.

Particles.vue
<template>
<vue-particles />
</template>
<script>
if (process.browser) {
require("vue-particles");
}

export default {
mounted() {
if (window.particlesJS) {
window.particlesJS.load();
}
},
};
</script>

위와 같이 로직을 변경해주니 IE11 에서도 정상 작동하였다.

여담

vue-particles 의 문서엔 완벽한 nuxt 호환이라 되어있지만 예외적인 상황이 있는 듯 하다.

· 15 min read

잊기 전에 슈도코드를 정리해놓자.

슈도코드

var 전체모듈 = [
function () {
const 합계함수 = (a, b) => a + b;
return 합계함수;
},

function () {
const 내부_합계함수 = 전체모듈[0]();
const 합계 = 내부_합계함수(10, 20);
console.log(합계);
return 합계;
},
];

let 시작모듈_인덱스 = 1;
전체모듈[시작모듈_인덱스]();

해석

배열에 다 때려넣고 호출해서 사용하는 방법이다 물론 내부는 더 복잡하다, 코드 스플리팅이 된다면 더더욱.

복잡한 내부

https://github.com/hg-pyun/minipack-kr/blob/master/src/minipack.js
/**
* @source https://github.com/hg-pyun/minipack-kr/blob/master/src/minipack.js
*
* 모듈 번들러들은 작은 코드 조각들을 웹 브라우저에서 실행될 수 있는 크고 복잡한 파일로 컴파일합니다.
* 이 작은 조각들은 단지 자바스크립트 파일들일 뿐이며, 이들 사이의 종속성은 모듈 시스템에 의해 표현됩니다
* (https://webpack.js.org/concepts/modules).
*
* 모듈 번들러들은 entry file 이라는 개념을 가지고 있습니다. 브라우저에 스크립트 태그를 몇개 추가하여
* 실행하는 대신, 번들 담당자에게 응용 프로그램의 메인 파일이 무엇인지 알려 줍니다. 이 파일이 어플리케이션을
* 실행하는 진입점이 됩니다.
*
* 번들러는 entry file의 의존성을 분석합니다. 그리고 그 다음 파일의 의존성을 파악합니다.
* 이 작업은 애플리케이션의 모든 모듈과 각 모듈이 서로 어떻게 의존하는지 파악할 때까지 반복됩니다.
*
* 이러한 프로젝트에 대한 이해를 종속성 그래프라 부릅니다.
*
* 이 예제에서는 종속성 그래프를 만들고 이 그래프를 사용하여 모든 모듈들을 하나의 번들로 패키징 합니다.
* 그럼 시작해 보겠습니다 :)
*
* 참고: 이 예제는 매우 단순화되어 있습니다. 순환 참조, 캐싱 모듈, 파싱 최적화 등에 대한 내용은 생략
* 하여 가능한가 단순하게 만들었습니다.
*/

const fs = require("fs");
const path = require("path");
const babylon = require("babylon");
const traverse = require("babel-traverse").default;
const { transformFromAst } = require("babel-core");

let ID = 0;

// 우선 file path를 받는 함수를 생성하고
// 파일을 내용을 읽고, 종속성을 추출합니다.
function createAsset(filename) {
// 파일의 내용을 문자열로 읽습니다.
const content = fs.readFileSync(filename, "utf-8");

// 이제 이 파일이 어떤 파일에 종속되는지 알아보겠습니다. 우리는 import 문자열을 보고 의존성을
// 파악할 수 있습니다 하지만, 이것은 단순한 접근법이어서, 대신에 자바스크립트 파서를 사용하겠습니다.

// 자바스크립트 파서들은 자바스크립트 코드를 읽고 이해할 수 있도록 도와주는 툴입니다.
// 파서는 AST(abstract syntax tree)라는 좀더 추상화된 모델을 생성합니다.
//
// AST에 대해 이해하려면 AST Explorer(https://astexplorer.net)을 꼭 보기를 강력하게 추천합니다.
// AST가 어떻게 이루어져 있는지 확인할 수 있습니다.
//
// AST는 우리의 코드에 대해 많은 정보를 가지고 있습니다. 우리는 쿼리를 이용하여
// 우리의 코드가 하려는 일에 대해 이해할 수 있습니다.
const ast = babylon.parse(content, {
sourceType: "module",
});

// 이 배열은 현재 모듈의 의존성을 상대 경로로 가지고 있을 것입니다.
const dependencies = [];

// 우리는 AST 순회를 통해 각각의 모듈들이 어떤 의존성을 가지고 있는지 이해하려 합니다.
// 이것을 통해 AST안에서 모든 import keyword 선언을 파악할 수 있습니다.
traverse(ast, {
// ECMAScript 모듈들은 정적이므로 매우 파악하기 쉽습니다.이는 변수를 가져올 수 없거나 조건부로
// 다른 모듈을 가져올 수 없음을 의미합니다. import 구분을 볼 때 마다 카운팅을 하고 의존성을 가지고
// 있는 것으로 간주 할 수 있습니다.
ImportDeclaration: ({ node }) => {
// import 구문마다 dependencies 배열에 값을 추가합니다.
dependencies.push(node.source.value);
},
});

// 또한 간단한 카운터를 이용하여 이 모듈에 고유 식별자를 할당합니다.
const id = ID++;

// 우리는 일부 브라우저에서만 지원하는 ECMAScript module들이나 기능들을 사용할 가능성도 있습니다.
// 우리가 만드는 번들이 모든 브라우저에서 돌아가도록 Babel을 이용해서 transpile할 수 있습니다
// (https://babeljs.io 참고).
//
// `presets` 옵션은 Babel이 어떻게 우리 코드를 바꿀지에 대해 결정합니다. 우리는 `babel-preset-env`
// 를 이용하여 대부분의 브라우저에서 우리의 코드를 사용할 수 있도록 바꾸도록 하겠습니다.
const { code } = transformFromAst(ast, null, {
presets: ["env"],
});

// 이 모듈에 대한 정보를 return 합니다.
return {
id,
filename,
dependencies,
code,
};
}

// 이제 단일 모듈의 종속성을 추출할 수 있으므로, entry file의 의존성을 추출하는 것부터 시작하겠습니다.
// 이 작업은 애플리케이션의 모든 모듈과 각 모듈이 서로 어떻게 의존하는지를 파악할 때까지 계속 진행할 것입니다.
// 이 작업의 결과물을 의존성 그래프라 부릅니다.
function createGraph(entry) {
// entry file부터 분석을 시작합니다.
const mainAsset = createAsset(entry);

// queue를 사용해서 모든 asset의 의존성을 분석하도록 하겠습니다. 이 작업을 위해
// entry asset을 가지고 있는 배열을 정의합니다.
const queue = [mainAsset];

// 여기서 queue의 반복을 위해 `for ... of` 반복문을 사용합니다. 처음에는 queue가 asset을 하나만
// 가지고 있지만 작업이 반복되는 동안에 새로운 asset들을 queue에 추가합니다. 이 반복문은 queue가
// 비어질 때 까지 계속됩니다.
for (const asset of queue) {
// 모든 asset들은 의존성이 있는 모듈에 대한 상대경로들을 리스트로 가지고 있습니다. 우리는 그 리스트를
// 순회하면서 `createAsset()`함수로 분석하고, 아래 객체를 통하여 모듈들의 의존성을 추척할 것입니다.
asset.mapping = {};

// 이것은 이 모듈이 있는 디렉토리입니다.
const dirname = path.dirname(asset.filename);

// 종속성에 대한 상대 경로 리스트를 순회합니다.
asset.dependencies.forEach((relativePath) => {
// `createAsset()` 함수는 절대 경로가 필요합니다. dependencies 배열은 상대 경로를 가지고
// 있는 배열입니다. 이러한 경로들은 모듈이 import된 file에 따라 상대적입니다. 따라서 부모 asset의
// 경로를 이용해서 상대 경로를 절대경로로 바꿔야 합니다.
const absolutePath = path.join(dirname, relativePath);

// asset의 내용울 분석하고, 내용을 읽고, 의존성을 추출합니다.
const child = createAsset(absolutePath);

// `asset`의 의존성은 `child`에게 달려있습니다. 우리는 `mapping` 객체에 relativePath와 child.id를
// 이용해서 관계를 표현할 수 있습니다.
asset.mapping[relativePath] = child.id;

// 마지막으로 child asset을 queue에 추가하여 구문 분석이 반복되도록 합니다.
queue.push(child);
});
}

// 이 시점에서 queue는 애플리케이션의 모든 모듈이 포함된 배열입니다. 이것이 우리가 그래프를 표현하는 방법입니다.
return queue;
}

// 다음으로, 그래프를 이용하여 브라우저에서 실행할 수 있는 번들을 반환하는 함수를 정의합니다.
//
// 우리의 번들은 self-invoking(자신을 부를수 있는)함수를 가지고 있습니다.
//
// (function() {})()
//
// 이 함수는 하나의 인자만 받을 수 있습니다: 모든 모듈의 정보를 가지고 있는 그래프.
function bundle(graph) {
let modules = "";

// 이 함수를 구성하기 전에 매개 변수로 전달할 객체를 만들겠습니다. 반드시 알아둬야할 것은 우리가 만드는
// 스트링은 2개의 중괄호({})로 감싸져 있어야 한다는 것입니다. 우리는 다음과 같은 포멧으로 추가할
// 것입니다: `key: value,`.
graph.forEach((mod) => {
// 그래프안에 있는 모든 모듈들은 entry를 객체로 가지고 있습니다. 우리는 module의 id를
// 값에 대한 키로 사용합니다.(각 모듈마다 2개의 값이 있습니다.)
//
// 찻번째 값은 함수로 감싼 각 모듈의 코드입니다. 그 이유는 모듈의 scope를 지정해야 하기 때문입니다.
// 한 모듈에서 변수를 정의하면 다른 모듈이나 글로벌 scope에 영향을 주지 않아야 합니다.
//
// transpiled된 모듈들은 CommonJS 모듈 시스템을 사용합니다:
// 해당 모듈 시스템은 `require`, `module`, 그리고 `exports`를 통해 모듈화 합니다.
// 이 키워드들은 일반적으로 브라우저에서 사용할수 없으므로, 우리의 함수를 이용하여 주입해야 합니다.
//
// 두번째 값은 모듈간의 의존성 매핑을 stringify하는 것입니다. 다음과 같은 객체입니다.
// { './relative/path': 1 }.
//
// transpiled된 우리의 모듈들이 상대경로와 합께 `require()`를 호출하기 때문입니다. 이 함수를 호출하면
// 그래프에서 이 모듈의 상대 경로에 해당하는 모듈을 확인할 수 있습니다.
modules += `${mod.id}: [
function (require, module, exports) { ${mod.code} },
${JSON.stringify(mod.mapping)},
],`;
});

// 마지막으로 self-invoking 함수의 body를 만듭니다.
//
// `require()` 함수를 만들며 시작하겠습니다: 모듈 id를 받아 앞서 만든 모듈 오브젝트에서 `module`을
// 찾습니다. function wrapper와 맵핑 객체를 얻기위해 two-value 객체를 이용합니다.
//
// 모듈의 코드는 모듈의 id들 대신 상대경로와 함께 `require()`함수를 호출합니다. 우리가 만든 require 함수는
// id 받습니다. 또한 두개의 모듈은 동일한 상대 경로를 요구할 수 있지만, 실제론 두개의 다른 모듈들을
// 의미하게 됩니다.
//
// 이 문제를 해결하기 위해 별도의 `require` 함수를 제공합니다. 모듈의 맵핑 오브젝트를 이용하여 상대경로를 ids에 할당합니다.
// 맵핑 오브젝트는 구체적인 모듈을 가져오기 위한 용도로, 상대 경로와 모듈 ids를 맵핑합니다.
//
// 마지막으로, 모듈이 require 되었을 때 exports 객체의 값이 노출되어야 합니다. 따라서 모듈 코드에 의해 변환 된
// `exports` 객체는 `require()`로 반환됩니다.
const result = `
(function(modules) {
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(name) {
return require(mapping[name]);
}
const module = { exports : {} };
fn(localRequire, module, module.exports);
return module.exports;
}
require(0);
})({${modules}})
`;

// 결과를 반환합니다. 만세! :)
return result;
}

const graph = createGraph("./example/entry.js");
const result = bundle(graph);

console.log(result);

여기의 번역된 내용을 확인해보자.

· 2 min read

window.scrollTo(0, 0) 를 아무리 해봐도 스크롤이 맨 위로 올라가지 않을 때 많은 구글링을 한 뒤 다음과 같은 해결책을 적용해보았다

  • react-scroll 라이브러리를 써보고
  • 혹시 window 가 레이아웃 컴포넌트에 갇혀있어서 그런가 window 를 App.js 에서 Top 기능을 쓰는 곳까지 내려보고
  • jQuery 를 추가해서도 해보고
  • 별 짓을 다하다가

아무 것도 안 되서 리프레쉬하러 옥상에 갔다가 갑자기 든 생각이 있었다

잠깐만 component 라는게 하나의 element 안에서 쇼를 하는 거 잖아. 그럼 element 안에서 스크롤을 이동하면?

해결

Footer.js
class Footer extends Component {
scrollToTop = (event) => {
document.getElementById("root").scrollTo(0, 0);
};

render() {
return (
<div>
<div className="top">
<a onClick={this.scrollToTop}>위로가기</a>
</div>
</div>
);
}
}

index.js 에서 ReactDOM 을 렌더링해주는 element(나같은 경우엔 div#root)를 찾아서 그 엘레먼트의 스크롤을 올려주자

여담

책에서 봤는데 뇌가 쉴 때 가장 좋은 아이디어가 나온다고 했는데, 사실인 것 같다

· One min read

express에 있는 sendStatus 기능을 활용하기 위해 start/hooks.js에도 다음 로직을 추가하자

소스

start/hooks.js
const { hooks } = require("@adonisjs/ignitor");

hooks.after.providersBooted(() => {
const Response = use("Adonis/Src/Response");

Response.macro("sendStatus", function (status) {
this.status(status).send("");
});
});

추가 후엔 controller에서 response.sendStauts(403)과 같은 응답을 반환할 수 있다

· 9 min read

react 로 form 을 만들어 사용하는 일은 생각보다 많은 걸 해야한다.

  • onSubmit에 이벤트를 잡아서 event.target.fieldName.value로 하나씩 체크를 해서 validating 을 하다가
  • 귀찮아질 때 쯤 function 을 만들어서 관리를 하고
  • 자주 사용하는 form input 을 컴포넌트로 만들며
  • form 에 초기값을 넣어줘야할 경우 (edit 페이지 같은) 값을 가져와 하나하나 바인딩하고
  • input 에 값이 바뀌었는지에 따라 submit 버튼을 활성, 비활성화 하는 로직도 있어야한다

번거로운 일을 하는 것 보다 redux-form 라이브러리를 사용해보자 검색시에 나오는 포스트들은 아주 기본적인 예제 밖에 없어서 문서를 매번 헤집는 시간이 필요했다.

먼저 이 라이브러리를 쓰기 위해선 다음 사전 지식이 필요하다

설치

$ yarn add redux-form

redux-form 의 리듀서를 연결시켜줘야한다

rootReducer
import { reducer as formReducer } from "redux-form";

const rootReducer = combineReducers({
// form 키를 사용해야한다
form: formReducer,
});

export default rootRecuder;

사용

redux-form component 와 redux-form 이 들어갈 compoent 를 만들어야한다 편의상 Login componentLoginForm component라고 하자

Login
import React, { Component } from "react";
import { connect } from "react-redux";
import LoginForm from "LoginForm";

class Login extends Component {
submitLoginForm = (formData) => {
console.log("LoginFormData => ", formData);
// this.props.loginAuth(formData)
};

render() {
return <LoginForm onSubmit={this.submitLoginForm} />;
}
}

export default connect()(Login);

LoginComponent에서는 딱히 특별한게 없이 LoginForm Component 를 호출하고 함수 하나를 내려준 게 끝이다

LoginForm
import React, { Component } from "react";
import { Field, reduxForm } from "redux-form";

class LoginForm extends Component {
render() {
const {
handleSubmit, // submit 이벤트를 redux-form에서 관리하게 수정
pristine, // form이 변경점이 없을 경우 true
submitting, // form이 submit 중일 경우 true
} = this.props;

return (
<form onSubmit={handleSubmit}>
<Field
name="id"
type="text"
component="input"
placeholder="아이디를 입력하세요"
/>
<Field
name="password"
type="password"
component="input"
placeholder="비밀번호를 입력하세요"
/>
</form>
);
}
}

export default reduxForm({
form: "LoginForm", // formReducer에 어떤 이름으로 이 폼을 등록할지
})(LoginForm);

redux-form에서 Field ComponentreduxForm High Order Component를 가져온 뒤 input tag 대신 Field Component 의 component 속성으로 input 을 만들어준다 그리고 해당 폼을 reduxForm 으로 감싸 redux 와 연결한다

handleSubmit은 onSubmit prop 으로 들어온 함수를 폼과 연결해준다 (event.preventDefault() 및 폼의 필드들을 serialize 해서 object 로 바꿔 onSubmit 함수의 parameter 로 넘겨준다)

말이 좀 어렵다면 Login.jssubmitLoginFormconsole.log(formData)의 구조를 보자

{
"id": "idValue",
"password": "passwordValue"
}

오 꽤 나이스하다 하지만 필드만 덩그라니 있는 경우는 없다 더 업그레이드 해보자

스타일이 필요한 필드

보통의 필드들은 label이 들어가 있고, selectBox, checkBox의 경우엔 스타일을 주기위해 대부분 외부 라이브러리를 쓴다 이럴 때 Field Component 의 component 속성에 원하는 모양을 만들어서 넣어주면 된다

LoginForm
...

const renderInputField = ({ input, type, label, placeholder, meta: { touched, error }}) => {
return (
<div>
<label>{label}</label>
<input {...input} type={type} placeholder={placeholder} />
{ touched && error &&
<span className="error">{error}</span>
}
</div>
)
}

class LoginForm extends Component {

render() {
const {
handleSubmit,
pristine,
submitting
} = this.props

return (
<form onSubmit={handleSubmit}>
<Field
name="id"
type="text"
label="아이디"
component={renderInputField}
placeholder="아이디를 입력하세요"
/>
<Field
name="password"
type="password"
label="비밀번호"
component={renderInputField}
placeholder="비밀번호를 입력하세요"
/>
</form>
)
}
}

renderInputField라는 함수형 컴포넌트를 만들어서 Field.component prop 에 넣긴했는데, div>input을 렌더링 해주는 것만 보이고 엄청난 파라미터들 때문에 정신이 아득할 것이다

component prop 에 함수형 컴포넌트를 넣으면 field prop 을 받게 된다 여기엔 내려준 prop 뿐 아니라 reduxForm 에서 넣어준 prop 도 존재한다 원래 해당 렌더링 컴포넌트는 아래와 같은 모습이다

const renderInputField = (field) => {
return <div>...</div>;
};

그럼 먼저 reduxForm 에서 넣어준 prop 을 보자

  • field.input: input 은 모든 HTML input attribute 를 가지고 있는게 아니라 다음 몇가지만 갖고 있다
    • checked
    • name
    • onBlur
    • onChange
    • onDragStart
    • onDrop
    • onFocus
    • value
  • field.meta: meta 는 해당 input 의 상태에 관한 데이터가 있다
    • touched: 해당 input 이 한 번이라도 클릭이 되어졌는지
    • valid: 해당 input 값이 valid 한지
    • error: 해당 input 값에 에러가 있으면 error message 가 들어온다
    • ... 나머진 문서를 참조하자

이제 type, label, placeholder 는 field.input prop 에 없어서 직접 넣어줬고, meta 는 이미 정의되어 있었구나 란 renderInputField 함수의 props 가 보일 것이고 Field Component 의 구조를 마음대로 변경할 수 있게 되었다

동적으로 value 를 바꿔야할 때

checkbox, select 의 경우엔 스타일이 들어가면 element 의 onClick 를 잡아서 hidden field 의 데이터를 바꿔줘야한다 checkbox 전체 영역을 클릭할 때마다 기존의 값을 toggle 해주는 component 를 만들 수 있다

LoginForm
const renderCheckboxField = ({ input, label }) => {
return (
<div onClick={(event) => input.onChange(!input.value)}>
<i
className={`fa ${!input.value ? "fa-square-o" : "fa-check-square-o"}`}
/>
<label>{label}</label>
<input type="hidden" {...input} />
</div>
);
};

value 에 따라서 font-awesome icon이 변경된다

함수로 값을 변경해야할 경우가 있을 수도 있다 이럴 땐 reduxForm prop 의 change 메소드를 사용하면 된다 바꿀 input 의 name 속성과 value 값을 넘겨주자

changeInputValue = (targetInputName, val) => {
this.props.change(targetInputName, val);
};

초기값

로그인 폼에는 아이디 저장 기능을 붙히면 폼이 초기화될 때 그 값을 가져와야한다

Login
...

class Login extends Component {
state = {
userId: ''
}

componentDidMount() {
const userId = localStorage.getItem('userId')

this.setState({
userId
})
}

render() {
return (
<LoginForm
onSubmit={this.submitLoginForm}
initialValues={{
id: this.state.userId
}}
/>
)
}
}

Login Component 에는 초기값을 가져와서 initialValues prop 안에 넣어준다

LoginForm
class LoginForm extends Component {
render() {
return <form>...</form>;
}
}

export default reduxForm({
form: "LoginForm",
// 이 값은 LoginForm 컴포넌트가 로드되고 나서
// initialValues가 바뀔 경우 폼 값도 업데이트가 되야되는지의 여부이다
enableReinitialize: true,
})(LoginForm);

localStorage.userId에 값이 들어있다면 그 값으로 id Field 가 초기화 된다 컴포넌트가 로딩되기 전에 값이 정해져있다면 enableReinitialize 옵션이 없어도 되지만, 수정 폼처럼 기존 데이터 fetch 후 또는 componentDidMount 후에 값을 가져온다면 해당 옵션값을 true로 넣어줘야한다

validation

여기까지 이해했다면 validation 은 큰 어려움 없이 문서의 예제소스를 참조하면 붙힐 수 있다

여담

react 라이브러리들은 읽은 뒤 바로 적용하긴 힘들다고 생각한다 매번 삽질이 필요한듯

· 3 min read

리액트 앱에서 번역기능을 사용하려면 react-intl 패키지를 사용하면 된다. 근데 Documentation을 보면 format 기능들, HOC로 데이터를 넣는 등 여러 기능 덕에 정신이 아득하다

step by step으로 간단하게 적용해보자

설치

$ npm install react-intl

번역 데이터 생성

편의상 root에 locale.js로 만들었다. 서비스시엔 locale 폴더에 언어별로 파일을 나눠 관리하자.

locale.js
export default {
en: {
hello: "Hello",
},
ko: {
hello: "안녕하세요",
},
ja: {
hello: "こんにちは",
},
};

연동

index.js
import { IntlProvider, addLocaleData } from "react-intl";
// 이 서브 라이브러리들이 내 locale 파일을 사용할 수 있게 해준다
import en from "react-intl/locale-data/en";
import ko from "react-intl/locale-data/ko";
import ja from "react-intl/locale-data/ja";
import locale from "./locale";

addLocaleData([...en, ...ko, ...ja]);

// 저장되어 있는 언어 데이터를 가져온다
const defaultLang = localStorage.getItem("lang") || "en";

ReactDOM.render(
<Provider store={store}>
<IntlProvider locale={defaultLang} messages={locale[defaultLang]}>
<App />
</IntlProvider>
</Provider>,
document.getElementById("root")
);

사용

import { FormattedMessage } from "react-intl";

<FormattedMessage id="hello" />;
// 저장되어 있는 언어 값에 따라 Hello, 안녕하세요, こんにちは 중 하나가 보여진다

inject

placeholder 등에서 텍스트만 필요할 때 component를 사용하지않고 다음과 같이 intl을 주입해서 사용한다.

import React, { Component } from "react";
import { injectIntl } from "react-intl";

class SignupForm extends Component {
render() {
const { intl } = this.props;

return (
<form>
<input
type="text"
name="id"
placeholder={intl.formatMessage({
id: "hello",
})}
/>
</form>
);
}
}

export default injectIntl(SignupForm);

child

HOC를 사용하지 않을 경우 FormattedMessage의 child로 번역된 문구를 받으면 된다

<FormattedMessage id="hello">
{(hello) => <input type="text" name="id" placeholder={hello} />}
</FormattedMessage>

dynamic

Hello, {Gracefullight} 처럼 동적으로 문구가 변해야할 경우 values prop을 활용하자 먼저, locale.js에서 변수가 될 부분을 {}로 감싸준다

locale.js
export default {
en: {
helloUser: "Hello {user}",
},
ko: {
helloUser: "{user} 안녕하세요",
},
ja: {
helloUser: "{user} こんにちは",
},
};

values 아래 object로 변수 값을 넣어주면 된다

<FormattedMessage
id="helloUser"
values={{
user: "Gracefullight",
}}
/>

여담

mo, po 파일 건들던 시간들이 너무 아깝다