์ค๋์ React ์ ํ๋ฆฌ์ผ์ด์ ์ ์ค์๊ฐ ์ค๋ฅ ์ถ์ ์ ๋ํด ์ค๋ช ํ๊ฒ ์ต๋๋ค. ํ๋ฐํธ์๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ผ๋ฐ์ ์ผ๋ก ์ค๋ฅ ์ถ์ ์ ์ฌ์ฉ๋์ง ์์ต๋๋ค. ์ผ๋ถ ํ์ฌ์์๋ ์ข ์ข ๋ฒ๊ทธ ์ถ์ ์ ๋ฏธ๋ฃจ๊ณ ๋ฌธ์ํ, ํ ์คํธ ๋ฑ์ ๋ง์น ํ์ ๋ฒ๊ทธ ์ถ์ ์ ๋ค์ ์์ํฉ๋๋ค. ๊ทธ๋ฌ๋ ์ ํ์ ๋ ๋์ ๋ฐฉํฅ์ผ๋ก ๋ณ๊ฒฝํ ์ ์๋ค๋ฉด ๊ทธ๋ ๊ฒ ํ์ญ์์ค!
1. ์ผํธ๋ฆฌ๊ฐ ํ์ํ ์ด์ ๋ ๋ฌด์์ ๋๊น?
๋๋ ๋น์ ์ด ์์ฐ ๊ณผ์ ์์ ๋ฒ๊ทธ๋ฅผ ์ถ์ ํ๋ ๋ฐ ๊ด์ฌ์ด ์๋ค๊ณ ๊ฐ์ ํฉ๋๋ค.
์ด๊ฒ์ด ์ถฉ๋ถํ์ง ์๋ค๊ณ ์๊ฐํ์ญ๋๊น?
์ข์์, ์ธ๋ถ ์ฌํญ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
๊ฐ๋ฐ์๊ฐ Sentry๋ฅผ ์ฌ์ฉํ๋ ์ฃผ์ ์ด์ :
- ์ค๋ฅ๊ฐ ์๋ ์ฝ๋๋ฅผ ๋ฐฐํฌํ ๋ ์ํ์ ๋ฐฉ์งํ ์ ์์ต๋๋ค.
- ์ฝ๋ ํ ์คํธ๋ก QA ์ง์
- ๋ฌธ์ ์ ๋ํ ๋น ๋ฅธ ์๋ฆผ ๋ฐ๊ธฐ
- ์ค๋ฅ๋ฅผ ๋น ๋ฅด๊ฒ ์์ ํ๋ ๋ฅ๋ ฅ
- ๊ด๋ฆฌ์ ํจ๋์์ ์ค๋ฅ๋ฅผ ํธ๋ฆฌํ๊ฒ ํ์ํ๊ธฐ
- ์ฌ์ฉ์/๋ธ๋ผ์ฐ์ ์ธ๊ทธ๋จผํธ๋ณ๋ก ์ค๋ฅ ์ ๋ ฌ
CEO/๋ฆฌ๋ ํ๋ก์ ํธ์ ์ฃผ์ ์ด์
- ๋น์ฉ ์ ๊ฐ(Sentry๋ ์๋ฒ์ ์ค์น ๊ฐ๋ฅ)
- ์ฌ์ฉ์ ํผ๋๋ฐฑ ๋ฐ๊ธฐ
- ์ค์๊ฐ์ผ๋ก ํ๋ก์ ํธ์ ๋ฌธ์ ์ ํ์
- ์ฌ๋๋ค์ด ์ฑ๊ณผ ๊ด๋ จํด ๊ฒช๊ณ ์๋ ๋ฌธ์ ์ ์๋ฅผ ์ดํดํ์ธ์.
- ๊ฐ๋ฐ์๊ฐ ์ค์ํ ๋ถ๋ถ์ ์ฐพ๋ ๋ฐ ๋์์ด ๋ฉ๋๋ค.
๊ฐ๋ฐ์๋ค์ด ์ด ๊ธ์ ๋จผ์ ๊ด์ฌ์ ๊ฐ์ง ๊ฒ์ด๋ผ๊ณ ์๊ฐํฉ๋๋ค. ๋ํ ์ด ์ด์ ๋ชฉ๋ก์ ์ฌ์ฉํ์ฌ ์์ฌ๊ฐ Sentry๋ฅผ ํตํฉํ๋๋ก ์ค๋ํ ์๋ ์์ต๋๋ค.
์ ์ฒด ๋ชฉ๋ก์ ๋ง์ง๋ง ํญ๋ชฉ์ ์ฃผ์ํ์ธ์.
์ด๋ฏธ ๊ด์ฌ์ด ์์ผ์ ๊ฐ์?
์ผํธ๋ฆฌ๋ ๋ฌด์์ธ๊ฐ์?
Sentry๋ ๊ฐ๋ฐ์๊ฐ ์ค์๊ฐ์ผ๋ก ์ถฉ๋์ ์ถ์ ํ๊ณ ์์ ํ๋ ๋ฐ ๋์์ด ๋๋ ์คํ ์์ค ๋ฒ๊ทธ ์ถ์ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋๋ค. ์ด ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฌ์ฉํ๋ฉด ํจ์จ์ฑ์ ๋์ด๊ณ ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํฌ ์ ์๋ค๋ ์ ์ ์์ง ๋ง์ญ์์ค. Sentry๋ JavaScript, Node, Python, PHP, Ruby, Java ๋ฐ ๊ธฐํ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด๋ฅผ ์ง์ํฉ๋๋ค.
2. ๋ก๊ทธ์ธ ํ ํ๋ก์ ํธ ์์ฑ
- Sentry ๊ณ์ ์ ๊ฐ์คํ์ธ์. ๋ก๊ทธ์ธํด์ผ ํ ์๋ ์์ต๋๋ค. (Sentry๋ ์๋ฒ์ ์ค์นํ ์ ์์ต๋๋ค.)
- ๋ค์ ๋จ๊ณ๋ ํ๋ก์ ํธ๋ฅผ ์์ฑํ๋ ๊ฒ์ ๋๋ค.
- ๋ชฉ๋ก์์ ์ธ์ด๋ฅผ ์ ํํ์ธ์. (์ฐ๋ฆฌ๋ React๋ฅผ ์ ํํ๊ฒ ์ต๋๋ค. "Create Project"๋ฅผ ํด๋ฆญํ์ธ์)
์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง์ถค์ค์ ํ์ธ์. Sentry๋ฅผ ์ปจํ ์ด๋์ ํตํฉํ๋ ๋ฐฉ๋ฒ์ ๋ํ ๊ธฐ๋ณธ ์๋ ์๋์์ ๋ณผ ์ ์์ต๋๋ค.
import * as Sentry from '@sentry/browser';
// Sentry.init({
// dsn: "<https://[email protected]/1432138>"
// });
// should have been called before using it here
// ideally before even rendering your react app
class ExampleBoundary extends Component {
constructor(props) {
super(props);
this.state = { error: null };
}
componentDidCatch(error, errorInfo) {
this.setState({ error });
Sentry.withScope(scope => {
Object.keys(errorInfo).forEach(key => {
scope.setExtra(key, errorInfo[key]);
});
Sentry.captureException(error);
});
}
render() {
if (this.state.error) {
//render fallback UI
return (
<a onClick={() => Sentry.showReportDialog()}>Report feedback</a>
);
} else {
//when there's not an error, render children untouched
return this.props.children;
}
}
}
Sentry์๋ ๋ค์์ ์ํํด์ผ ํ ์์ ์ ํ์ ํ๋ ๋ฐ ๋์์ด ๋๋ ์ ์ฉํ ๋ง๋ฒ์ฌ๊ฐ ์์ต๋๋ค. ๋ค์ ๋จ๊ณ๋ฅผ ์ํํ ์ ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ์ค๋ฅ ์ฒ๋ฆฌ๊ธฐ๋ฅผ ๋ง๋๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ ๋๋ฆฌ๊ณ ์ถ์ต๋๋ค. ์ข์ต๋๋ค. ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์์ต๋๋ค! ๋ค์ ๋จ๊ณ๋ก ๋์ด๊ฐ์
3. React์ Sentry ํตํฉ
ํ๋ก์ ํธ์ npm ํจํค์ง๋ฅผ ์ค์นํด์ผ ํฉ๋๋ค.
npm i @sentry/browser
์ปจํ ์ด๋์์ Sentry๋ฅผ ์ด๊ธฐํํฉ๋๋ค.
Sentry.init({
// dsn: #dsnUrl,
});
DSN์ ํ๋ก์ ํธ -> ์ค์ -> ํด๋ผ์ด์ธํธ ํค์ ์์ต๋๋ค. ๊ฒ์์ฐฝ์์ ํด๋ผ์ด์ธํธ ํค๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.
componentDidCatch(error, errorInfo) {
Sentry.withScope(scope => {
Object.keys(errorInfo).forEach(key => {
scope.setExtra(key, errorInfo[key]);
});
Sentry.captureException(error);
});
}
4. ์ฒซ ๋ฒ์งธ ์ค๋ฅ ์ถ์
์๋ฅผ ๋ค์ด Deezer API์ ํจ๊ป ๊ฐ๋จํ ์์
์ ํ๋ฆฌ์ผ์ด์
์ ์ฌ์ฉํ์ต๋๋ค. ๋น์ ์ ๊ทธ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค
ํธ์ถํ๋ ๋ฒํผ์ ๋ง๋ค์ด์ผ ํฉ๋๋ค. console.log ั ์ฌ์ฉ์.์ด๋ฉ์ผ. ์ด ์์
ํ์๋ ๋ค์๊ณผ ๊ฐ์ ์ค๋ฅ ๋ฉ์์ง๊ฐ ํ์๋ฉ๋๋ค. ์กํ์ง ์์ TypeError(์ ์๋์ง ์์ ์์ฑ์ ์ฝ์ ์ ์์ต๋๋ค) email
) ์ฌ์ฉ์ ๊ฐ์ฒด๊ฐ ๋๋ฝ๋์๊ธฐ ๋๋ฌธ์
๋๋ค. ๋น์ ์ ๋ํ ์ฌ์ฉํ ์ ์์ต๋๋ค ์๋ฐ์คํฌ๋ฆฝํธ ์์ธ.
<button type="button" onClick={() => console.log(user.email)}>
Test Error button
</button>
์ ์ฒด ์ปจํ ์ด๋๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
import React, { Component } from "react";
import { connect } from "react-redux";
import { Input, List, Skeleton, Avatar } from "antd";
import * as Sentry from "@sentry/browser";
import getList from "../store/actions/getList";
const Search = Input.Search;
const mapState = state => ({
list: state.root.list,
loading: state.root.loading
});
const mapDispatch = {
getList
};
class Container extends Component {
constructor(props) {
super(props);
Sentry.init({
dsn: "https://[email protected]/1417586",
});
}
componentDidCatch(error, errorInfo) {
Sentry.withScope(scope => {
Object.keys(errorInfo).forEach(key => {
scope.setExtra(key, errorInfo[key]);
});
Sentry.captureException(error);
});
}
render() {
const { list, loading, getList } = this.props;
const user = undefined;
return (
<div className="App">
<button
type="button"
onClick={() => console.log(user.email)}
>
test error1
</button>
<div onClick={() => Sentry.showReportDialog()}>Report feedback1</div>
<h1>Music Finder</h1>
<br />
<Search onSearch={value => getList(value)} enterButton />
{loading && <Skeleton avatar title={false} loading={true} active />}
{!loading && (
<List
itemLayout="horizontal"
dataSource={list}
locale={{ emptyText: <div /> }}
renderItem={item => (
<List.Item>
<List.Item.Meta
avatar={<Avatar src={item.artist.picture} />}
title={item.title}
description={item.artist.name}
/>
</List.Item>
)}
/>
)}
</div>
);
}
}
export default connect(
mapState,
mapDispatch
)(Container);
์ด ๋ฒํผ์ ํตํฉํ ํ ๋ธ๋ผ์ฐ์ ์์ ํ ์คํธํด์ผ ํฉ๋๋ค.
์ฒซ ๋ฒ์งธ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.
์ฐํ!
ํค๋ ์ค๋ฅ๋ฅผ ํด๋ฆญํ๋ฉด ์คํ ์ถ์ ์ด ํ์๋ฉ๋๋ค.
๋ฉ์์ง๊ฐ ์ข์ง ์์ ๊ฒ ๊ฐ์ต๋๋ค. ๋ฌผ๋ก ์ฐ๋ฆฌ๋ ์ฝ๋๊ฐ ์ด๋์ ์๋์ง ์ดํดํ์ง ๋ชปํ ์ฑ ์ค๋ฅ ๋ฉ์์ง๋ฅผ ๋ณด์์ต๋๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก ReactJS์ ์์ค ๋งต์ ๊ตฌ์ฑ๋์ง ์์๊ธฐ ๋๋ฌธ์ ์ด์ ๋ํด ์ด์ผ๊ธฐํ๊ณ ์์ต๋๋ค.
๋ํ ์์ค ๋งต ์ค์ ์ ๋ํ ์ง์นจ๋ ์ ๊ณตํ๊ณ ์ถ์ง๋ง ๊ทธ๋ ๊ฒ ํ๋ฉด ์ด ๊ธ์ด ์๋ํ ๊ฒ๋ณด๋ค ํจ์ฌ ๊ธธ์ด์ง ๊ฒ์ ๋๋ค.
์ด ์ฃผ์ ๋ฅผ ๊ณต๋ถํ ์ ์์ต๋๋ค
5. ์ฌ์ฉ ๋ณด์ด ๋์ ์์ API
์ข์์. ์ฐ๋ฆฌ๋ ์ด์ ๋จ๋ฝ์์ ์๋ฐ์คํฌ๋ฆฝํธ ์์ธ๋ฅผ ๋ค๋ฃจ์์ต๋๋ค. ๊ทธ๋ฌ๋ XHR ์ค๋ฅ์ ๋ํด์๋ ์ด๋ป๊ฒ ํด์ผ ํ ๊น์?
Sentry์๋ ์ฌ์ฉ์ ์ ์ ์ค๋ฅ ์ฒ๋ฆฌ ๊ธฐ๋ฅ๋ ์์ต๋๋ค. API ์ค๋ฅ๋ฅผ ์ถ์ ํ๋ ๋ฐ ์ฌ์ฉํ์ต๋๋ค.
Sentry.captureException(err)
์ค๋ฅ ์ด๋ฆ, ์์ค, ๋ฐ์ดํฐ ์ถ๊ฐ, ์ ํ๋ฆฌ์ผ์ด์ , ์ด๋ฉ์ผ ๋ฑ์ ์ฌ์ฉํ์ฌ ๊ณ ์ ํ ์ฌ์ฉ์ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉ์ ์ ์ํ ์ ์์ต๋๋ค.
superagent
.get(`https://deezerdevs-deezer.p.rapidapi.com/search?q=${query}`)
.set("X-RapidAPI-Key", #id_key)
.end((err, response) => {
if (err) {
Sentry.configureScope(
scope => scope
.setUser({"email": "[email protected]"})
.setLevel("Error")
);
return Sentry.captureException(err);
}
if (response) {
return dispatch(setList(response.body.data));
}
});
catch API์ ์ผ๋ฐ ํจ์๋ฅผ ์ฌ์ฉํ๊ณ ์ถ์ต๋๋ค.
import * as Sentry from "@sentry/browser";
export const apiCatch = (error, getState) => {
const store = getState();
const storeStringify = JSON.stringify(store);
const { root: { user: { email } } } = store;
Sentry.configureScope(
scope => scope
.setLevel("Error")
.setUser({ email })
.setExtra("store", storeStringify)
);
// Sentry.showReportDialog(); - If you want get users feedback on error
return Sentry.captureException(error);
};
API ํธ์ถ์์ ์ด ํจ์๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
export default query => (dispatch, getState) => {
superagent
.get(`https://deezerdevs-deezer.p.rapidapi.com/search?q=${query}`)
.set("X-RapidAPI-Key", #id_key)
.end((error, response) => {
if (error) {
return apiCatch(error, getState)
}
if (response) {
return dispatch(setList(response.body.data));
}
});
};
๋ฐฉ๋ฒ์ ํ์ธํด ๋ด ์๋ค:
- ์ธํธ๋ ๋ฒจ ์ผํธ๋ฆฌ ๋์๋ณด๋์ ๋ ๋ฒจ ์ค๋ฅ๋ฅผ ์ฝ์ ํ ์ ์์ต๋๋ค. ์ฌ๊ธฐ์๋ '์น๋ช ์ ', '์ค๋ฅ', '๊ฒฝ๊ณ ', '๋ก๊ทธ', '์ ๋ณด, '๋๋ฒ๊ทธ', '์ค์') ์์ฑ์ด ์์ต๋๋ค.
- ์ฌ์ฉ์ ์ค์ ๋ชจ๋ ์ฌ์ฉ์ ๋ฐ์ดํฐ(ID, ์ด๋ฉ์ผ ์ฃผ์, ๊ฒฐ์ ๊ณํ ๋ฑ)๋ฅผ ์ ์ฅํ๋ ๋ฐ ๋์์ด ๋ฉ๋๋ค.
- ์ธํธ์์คํธ๋ผ ์๋ฅผ ๋ค์ด ์ ์ฅ๊ณผ ๊ฐ์ด ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์ง์ ํ ์ ์์ต๋๋ค.
๋ฒ๊ทธ์ ๋ํ ์ฌ์ฉ์ ํผ๋๋ฐฑ์ ์ป์ผ๋ ค๋ฉด showReportDialog ๊ธฐ๋ฅ์ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
Sentry.showReportDialog();
๊ฒฐ๋ก :
์ค๋ ์ฐ๋ฆฌ๋ Sentry๋ฅผ React ์ ํ๋ฆฌ์ผ์ด์ ์ ํตํฉํ๋ ํ ๊ฐ์ง ๋ฐฉ๋ฒ์ ์ค๋ช ํ์ต๋๋ค.
โ ํ
๋ ๊ทธ๋จ ์ฑํ
์ถ์ฒ : habr.com