자바스크립트에서 날짜는 Date 객체로 다루곤 한다.

1
2
3
const date = new Date();

console.log(date); // Tue Aug 01 2023 22:32:45 GMT+0900 (Korean Standard Time)
  • 1970년 1월 1일 UTC(국제표준시) 자정으로부터 지난 시간을 밀리초로 나타낸다.

Date 객체도 다양한 메서드를 제공하는데, 개발자가 이를 다루기에 직관적이지 못하여 다루기가 까다롭다.

매번 chatGPT한테 물어볼 수도 없고 ..ㅎㅎ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const date = today.getDate();
const day = today.getDay();
const month = today.getMonth();
const hours = today.getHours();
const minutes = today.getMinutes();
const ms = today.getMilliseconds();
const sec = today.getSeconds();

console.log(today); // Tue Aug 01 2023 22:32:45 GMT+0900 (Korean Standard Time)
console.log(date); // 1
console.log(day); // 2 (0~6) 일 ~ 토
console.log(month); // 7 (0~11)
console.log(hours); // 22
console.log(minutes); // 32
console.log(ms); // 122
console.log(sec); // 45
  • day를 숫자로 표현해주고 있으니 직관적이지 못하다. 우리나라는 월요일부터 시작으로 세는 사람이 많은데 일요일부터 시작하니…
  • month도 왜 0부터 시작하는 것인지.. ㅋㅋ

그래서 날짜를 좀 더 쉽게 다루기 위해 다양한 날짜 라이브러리가 나왔다.

오늘은 그 중 date-fns 라이브러리를 알아볼 것이다.

date-fns

1
npm install date-fns --save

설치는 다음 명령어로 설치하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { format, compareAsc } from "date-fns";

format(new Date(2014, 1, 11), "MM/dd/yyyy");
//=> '02/11/2014'

const dates = [
new Date(1995, 6, 2),
new Date(1987, 1, 11),
new Date(1989, 6, 10),
];
dates.sort(compareAsc);
//=> [
// Wed Feb 11 1987 00:00:00,
// Mon Jul 10 1989 00:00:00,
// Sun Jul 02 1995 00:00:00
// ]
  • format 메서드를 사용하여 날짜를 원하는 형식으로 바꿀 수 있다.

그 외에도 날짜를 서로 비교하여 우선순위를 매기거나 날짜를 더하고 뺄 수 있는 등 다양한 메서드를 사용하여 자유자재로 날짜를 다룰 수 있다.

예시로 날짜를 빼주는 메서드인 sub메서드만 알아보자.

1
2
3
4
5
6
7
8
9
10
11
// Subtract the following duration from 15 June 2017 15:29:20
const result = sub(new Date(2017, 5, 15, 15, 29, 20), {
years: 2,
months: 9,
weeks: 1,
days: 7,
hours: 5,
minutes: 9,
seconds: 30,
});
//=> Mon Sep 1 2014 10:19:50
  • 2017년에서 2년을 뺀다.
  • 6월에서 9개월을 뺀다.
  • 15일에서 1주일(7일)을 뺀다
  • 나머지 8일에서 7일을 뺀다.
  • 15시간에서 5시간을 뺀다.
  • 29분에서 9분을 뺀다.
  • 20초에서 30초를 뺀다.

결과값은 2014년 9월 1일 월요일 10시 19분 50초가 나온다.

댓글 공유

저번 포스팅 때, Legend에 hover했을 때, 해당 데이터만 highlight 되도록 구현을 했다.

이번에는 Legend를 커스터마이징하여 색상도 바꿔보도록 하려고한다.

1. CustomLegend 컴포넌트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const CustomLegend = (props: any) => {
const { payload, onMouseEnter, onMouseLeave } = props;

return (
<ul>
{payload.map((entry: any, index: any) => (
<li
key={`item-${index}`}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
style={{ listStyle: "none", color: colors.GRAPH[`${index + 1}`] }}
>
{entry.value}
</li>
))}
</ul>
);
};
  • 예시를 위해 타입은 any로 설정하였다.
  • Legend의 각 li에 mouse 이벤트를 할당하였다.
  • 마우스 이벤트는 호버된 데이터를 제외한 데이터들의 opacity를 줄여서 해당 데이터만 highlight 되도록 한다.

2. CustomLegend의 props

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const handleMouseEnter = (o: any) => {
const dataKey = o.target.innerHTML;

const entries = Object.entries(opacity).map(([key, value]) =>
key === dataKey ? [key, 1] : [key, 0.2]
);

const mappedObj: any = entries.reduce((prev, curr) => {
const [key, value] = curr;
prev = { ...prev, [key]: value };
return prev;
}, {});

setOpacity(mappedObj);
};

그런데, 해당 데이터에 호버를 해도 모든 데이터의 opacity가 줄어드는 문제가 발생했다.

그 이유는 Legend에서의 props와 customLegend의 props가 달라서 mouse 이벤트가 잘못 동작했기 때문이다.

  • 이전 mouse 이벤트에서는 props안에 dataKey 속성으로 호버된 데이터 값을 가져올 수 있었다.
  • 하지만 customLegend에서는 props에 너무나도 많은 속성이 있었고 이 중 나는 target속성의 innerHTML 속성으로 호버된 데이터의 dataKey를 확인하는 로직을 구성하였다.

3. 전체 코드

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import { useEffect, useState } from "react";
import {
LineChart,
Line,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";
import colors from "styles/colors";

const CustomLegend = (props: any) => {
const { payload, onMouseEnter, onMouseLeave } = props;

return (
<ul>
{payload.map((entry: any, index: any) => (
<li
key={`item-${index}`}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
style={{ listStyle: "none", color: colors.GRAPH[`${index + 1}`] }}
>
{entry.value}
</li>
))}
</ul>
);
};

function NewPortChart() {
const [opacity, setOpacity] = useState<any>({});

const handleMouseEnter = (o: any) => {
const dataKey = o.target.innerHTML;

const entries = Object.entries(opacity).map(([key, value]) =>
key === dataKey ? [key, 1] : [key, 0.2]
);

const mappedObj: any = entries.reduce((prev, curr) => {
const [key, value] = curr;
prev = { ...prev, [key]: value };
return prev;
}, {});

setOpacity(mappedObj);
};

const handleMouseLeave = () => {
const entries = Object.entries(opacity).map(([key, value]) => [key, 1]);

const mappedObj: any = entries.reduce((prev, curr) => {
const [key, value] = curr;
prev = { ...prev, [key]: value };
return prev;
}, {});

setOpacity(mappedObj);
};

useEffect(() => {
const mappedOpacity = Object.keys(data[0]).reduce((prev, curr) => {
prev = { ...prev, [curr]: 1 };
return prev;
}, {});
setOpacity(mappedOpacity);
}, []);

return (
<ResponsiveContainer width="100%" height="100%">
<LineChart width={857} height={440} data={data}>
<Tooltip />
<Legend
align="right"
verticalAlign="middle"
layout="vertical"
content={
<CustomLegend
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
}
/>
{Object.keys(data[0]).map((key, index) => (
<Line
key={key}
type="linear"
dataKey={key}
strokeOpacity={opacity[key]}
strokeLinecap="round"
stroke={colors.GRAPH[`${index + 1}`]}
activeDot={false}
dot={false}
/>
))}
</LineChart>
</ResponsiveContainer>
);
}

export default NewPortChart;

기본 CustomLegend

기본 customlegend

Hover된 데이터만 highlight

hover1
hover2
hover3

추가로…

색상만 바꿀 것이였다면 왜 CustomLegend까지 쓰면서 복잡하게 시도를 했을까 궁금증이 들 수도 있다.

디자이너 요구사항이 Legend와 해당 Line 데이터 끝 부분을 선으로 연결해달라는 요청이 있었기 때문에 CustomLegend를 사용해보았다.

아직 해당 부분은 좀 더 고민이 필요하기 때문에 추후에 포스팅하도록 하겠다.

댓글 공유

서비스가 주식 관련 서비스이다 보니 차트를 사용할 일이 잦다.

차트를 직접 구현하자니 너무 공수가 많이 들 것 같아 Recharts 라이브러리를 자주 사용하고 있다.

하지만 공식문서에서 모든게 나와있지 않아서 ChatGPT의 도움도 많이 받고 있다. 그래도 꽤 쓸만한 라이브러리이다.

오늘은 Recharts 라이브러리로 legend에 hover했을 때, hover된 데이터만 highlight 되도록 구현해볼 것이다.

chart

우선 기본적인 Line 차트를 렌더링한다.

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import { useState } from "react";
import {
LineChart,
Line,
XAxis,
YAxis,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";

const data = [
{
name: "Page A",
uv: 4000,
pv: 2400,
amt: 2400,
},
{
name: "Page B",
uv: 3000,
pv: 1398,
amt: 2210,
},
{
name: "Page C",
uv: 2000,
pv: 9800,
amt: 2290,
},
{
name: "Page D",
uv: 2780,
pv: 3908,
amt: 2000,
},
{
name: "Page E",
uv: 1890,
pv: 4800,
amt: 2181,
},
{
name: "Page F",
uv: 2390,
pv: 3800,
amt: 2500,
},
{
name: "Page G",
uv: 3490,
pv: 4300,
amt: 2100,
},
];

function MultiLineCharts() {
const [opacity, setOpacity] = useState({ uv: 1, pv: 1 });

const handleMouseEnter = (o) => {
const { dataKey } = o;

const entries = Object.entries(opacity).map(([key, value]) =>
key === dataKey ? [key, 1] : [key, 0.2]
);

const mappedObj = entries.reduce((prev, curr) => {
const [key, value] = curr;
prev = { ...prev, [key]: value };
return prev;
}, {});

setOpacity(mappedObj);
};

const handleMouseLeave = (o) => {
const { dataKey } = o;

const entries = Object.entries(opacity).map(([key, value]) => [key, 1]);

const mappedObj = entries.reduce((prev, curr) => {
const [key, value] = curr;
prev = { ...prev, [key]: value };
return prev;
}, {});

setOpacity(mappedObj);
};
return (
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer width="100%" height={300}>
<LineChart
width={500}
height={300}
data={data}
margin={{
top: 5,
right: 30,
left: 20,
bottom: 5,
}}
>
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
<Line
type="monotone"
dataKey="pv"
strokeOpacity={opacity.pv}
stroke="#8884d8"
activeDot={{ r: 8 }}
/>
<Line
type="monotone"
dataKey="uv"
strokeOpacity={opacity.uv}
stroke="#82ca9d"
/>
</LineChart>
</ResponsiveContainer>
</ResponsiveContainer>
);
}

export default MultiLineCharts;
  • Line 그래프의 opacityuseState로 관리한다.
  • Legend에 onMouseEnter, onMouseLeave 이벤트를 할당한다.
  • 여기서 유저가 이벤트를 발생시킨 요소만 opacity를 두고 나머지 데이터들의 opacity를 줄여주기 위해 opacity 객체를 재구성했다.
  • Object.entries()reduce()를 사용하여 편리하게 객체를 재구성할 수 있다.

결과

hover chart

  • 보라색 Legend Hover 시

hover_chart2

  • 연두색 Legend Hover 시

댓글 공유

간단하게 Reset CSS 적용하기

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
*,
*::before,
*::after {
box-sizing: border-box;
}

* {
margin: 0;
padding: 0;
font: inherit;
}

html {
color-scheme: dark light;
}

body {
min-height: 100vh;
}

img,
picture,
svg,
video {
display: block;
max-width: 100%;
}

댓글 공유

그리드로 footer 만들기

grid 푸터 예시

우리는 푸터를 만들 때, 위와 같이 푸터를 하단에 고정하기 위해 고민한다.

나도 에이블 프로젝트를 할 때, 고민을 많이 했었고, position을 썼었던 걸로 기억하는데 깔끔하게 처리하지 못했었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div class="main-layout">
<header>
SIMPLIFY YOUR CSS WITH THESE 3 GRID LAYOUT SOLUTIONS Lorem ipsum dolor, sit
amet consectetur adipisicing elit. Enim fugiat fuga illum doloribus
perferendis asperiores ab voluptatem laudantium, dignissimos nulla. Nemo
minus aliquid nesciunt quos temporibus ratione dicta quas doloremque.
</header>

<main>
<h1>Title</h1>
<p>Contents</p>
</main>

<footer>
<div>
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Nesciunt soluta
hic, odit ad quisquam iste? Magnam, animi ut, tempore libero a aliquam
vitae quos alias possimus fugiat officia, temporibus illo!
</div>
</footer>
</div>

위와 같은 HTML 구조를 가지는 예시를 들어보자.

1
2
3
4
5
6
.main-layout {
min-height: 100vh;

display: grid;
grid-template-rows: auto 1fr auto;
}
  • header, main, footer 를 감싸는 컨테이너의 min-height100vh로 화면에 꽉차게 설정
  • grid 속성을 주어 빈 공간이 없게 만든다.
  • grid의 rows 속성의 너비를 지정한다.
    • auto로 설정하면 해당 태그가 가지고 있는 높이만큼만 설정하게된다.

댓글 공유

개발을 하다보면 디자이너나 클라이언트의 요구사항을 만족시키기 위해 기본 input 태그나 select 태그 등을 커스텀 해야하는 경우가 많다.

문제

커스텀 select 태그를 만들어서 아이콘도 img 태그를 사용하여 추가해주었다.

하지만, select 태그 내부의 icon을 클릭하게 되면 select 태그가 열리지 않는 불편함이 있다.

해결

아이콘에 pointer-events:none; 속성을 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.custom-select {
width: 200px;
position: relative;

select {
width: 100%;
height: 24px;
}
}

.select-icon {
position: absolute;
top: 5px;
right: 5px;
pointer-events: none;
}
  • 이렇게 하면 아이콘을 클릭해도 select 태그가 클릭된 것처럼 제대로 동작한다.

댓글 공유

여러 input 값 상태 관리하기

이번 프로젝트를 하다가 한 페이지에 3가지 케이스의 멀티 input을 가지는 화면이 렌더링되는 부분이 있었다.

가령 코드 구조는 다음과 같다.

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
34
35
36
37
38
39
40
41
42
43
// Backtest.tsx
import { ChangeEvent, useState } from "react";
import { useRecoilState } from "recoil";
import { backtestState } from "../atom";
import BacktestInputs from "../components/BacktestInputs/BacktestInputs";

function Backtest() {
const [backtest, setBacktest] = useRecoilState(backtestState);

const [backtestInputs, setBacktestInputs] = useState({
invest: "",
port: "",
dol: "",
});
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
const { value, name } = e.target;
setBacktestInputs({ ...backtestInputs, [name]: value });
};

const onSave = () => {
setBacktest(backtestInputs);
};

const onReset = () => {
setBacktestInputs({ invest: "", port: "", dol: "" });
};
return (
<>
<button onClick={onSave}>저장</button>
<BacktestInputs value={backtestInputs} onChange={onChange} />
<button onClick={onReset}>input 값 리셋</button>

<h2>전역상태</h2>
<ul>
<li>invest: {backtest["invest"]}</li>
<li>port: {backtest["port"]}</li>
<li>dol: {backtest["dol"]}</li>
</ul>
</>
);
}

export default Backtest;
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// BacktestInputs.tsx
import styled from "@emotion/styled";
import { ChangeEvent } from "react";

interface Props {
value: { invest: string, port: string, dol: string };
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
}

function BacktestInputs({ value, onChange }: Props) {
const { invest, port, dol } = value;

return (
<>
<h1>Backtest</h1>
<InputWrap>
<label htmlFor="invest">Investment: </label>
<input
type="text"
name="invest"
value={invest}
onChange={onChange}
id="invest"
/>
</InputWrap>
<InputWrap>
<label htmlFor="port">Port: </label>
<input
type="text"
name="port"
value={port}
onChange={onChange}
id="port"
/>
</InputWrap>
<InputWrap>
<label htmlFor="dol">Dual: </label>
<input
type="text"
name="dol"
value={dol}
onChange={onChange}
id="dol"
/>
</InputWrap>
</>
);
}

export default BacktestInputs;

const InputWrap = styled.div`
height: 50px;
`;

나는 고민을 하였다. Backtest 페이지에서 Input을 감싸고 있는 컴포넌트가 3종류가 있고, 3종류의 컴포넌트 내부에는 다수의 input 값을 가지고 있을 때, 어떻게 input을 관리하면 좋을지 고민했다.

그렇게 하다가 생각해낸 방법이 각 컴포넌트의 input 값의 name을 가지는 객체로 input들의 상태를 관리하기로 했다.

전역상태와 input 상태

나는 전역상태를 직접 onChange 이벤트가 일어날 때 마다 바꿔주는 코드를 작성했었다. 전역상태는 최소한으로 조작을 해야지만 에러가 덜 발생하기 때문에 최종적으로 저장 버튼을 클릭했을 때만 전역상태에 저장하기로 했다.

그래서 input 상태를 객체로 가지고 저장 버튼을 클릭했을 때, 전역 상태 update 함수에게 input 상태를 파라미터로 전달하도록 구현하였다.

소감

처음에는 그 많은 input을 직접 전달해줘야하나 … 걱정을 했었다. 하지만 객체로 input을 관리하고, input 마다 name 속성을 사용하여 onChange 이벤트로 controlled input을 다룰 수 있다는 것을 알게되어서 앞으로 다중 input 상태관리는 이러한 방법으로 접근하면 된다는 것을 알게되었다.

댓글 공유

React로 카카오 로그인 연동하기

이번 프로젝트에서 카카오 로그인 기능을 구현하였다. 배우는 곳은 무조건 공식문서가 짱이지 !

하고 공식문서에 들어가봤다.

인증과 인가

인증과인가

오… OAuth 2.0 기반으로 인증과 인가를 간편, 안전하게 처리할 수 있다는 장점이 있군?!

토큰

토큰이라는 사용자의 카카오 로그인 인증 및 인가 정보를 담은 권한 증명을 제공한다. Access Token, Refresh Token 두 종류를 발급해준다.

token

로그인 과정 이해하기

로그인 과정

1단계. 카카오 로그인

  1. 클라이언트에서 백엔드서버로 로그인 요청을 보낸다.

  2. 백엔드 서버에서 인가 코드를 카카오에게 요청한다.

  3. 카카오는 인증과 동의 요청을 클라이언트에게 보낸다.

  4. 클라이언트가 동의 항목 체크하고 로그인한다.

  5. 앱에 등록된 Redirect URI로부터 인가 코드를 받고 이를 가지고 토큰을 요청한다.

  6. 카카오가 토큰을 발급해준다.

  7. 카카오 로그인이 완료된다 !

2단계. 회원 확인 및 가입

  1. 클라이언트가 발급받은 토큰으로 카카오 API 서버로 요청을 보내 사용자 정보를 가져온다.

  2. 카카오 API 서버는 요청의 토큰을 검증하여 처리한다.

  3. 백엔드 서버는 제공받은 사용자 정보로 서비스 회원인지 확인하고 신규 사용자인 경우 회원가입 시킨다.

3단계. 서비스 로그인

  1. 백엔드에서 세션을 발급해주고 클라이언트에서 로그인 완료 처리해준다.

필수 설정 항목

1. 플랫폼 등록

나의 서비스를 연결해줄 플랫폼을 등록한다.

플랫폼 등록에 가보면 Android, ios, Web을 선택할 수 있고 생성하면 API Key를 발급해준다.

  • 사이트 도메인을 설정해줘야한다.
  • http://, https://, file:// 형식의 도메인을 등록할 수 있으며, http와 https 도메인은 둘 중 한 가지만 등록해도 사용할 수 있습니다.
  • 최대 10개의 도메인을 등록할 수 있습니다.

2. 카카오 로그인 활성화

카카오 로그인에 가서 활성화 버튼을 ON으로 바꿔줘야한다.

3. Redirect URI 등록

인가 코드를 요청받은 URI를 등록해준다.

1
http://localhost:3000/oauth/callback/kakao

해당 Redirect URI로 요청을 보낼 때 필수 파라미터들이 있다.

  • cliend_id : 내 애플리케이션의 REST API 앱 키
  • redirect_uri : Redirect URI
  • response_type : code 고정값

4. 동의 항목

어떤 동의항목을 받을 것인지 설정해준다. 필수만 체크해주자 편의상

5. 구현 방법 선택

  • REST API
  • JavaScript
  • Android
  • IOS
  • Flutter

로 구현할 수 있는데, REST API로 하자.

실습

1. 카카오 로그인 요청을 보낼 버튼을 하나 만들자

1
2
3
// api_key.js
export const REST_API_KEY = `alksdjfoasidnfaofj`;
export const REDIRECT_URI = "http://localhost:3000/auth/callback/kakao";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { REST_API_KEY, REDIRECT_URI } from "./api_key.js";

const App = () => {
const KAKAO_AUTH_URL = `https://kauth.kakao.com/oauth/authorize?client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}&response_type=code`;

const handleLogin = () => {
window.location.href = KAKAO_AUTH_URL;
};

return (
<div className="App">
<button onClick={handleLogin}>카카오 로그인</button>
</div>
);
};
1
2
GET /oauth/authorize?client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}&response_type=code HTTP/1.1
Host: kauth.kakao.com
  • 카카오 로그인 인가 코드받기 부분에 가면 Host와 GET 요청 URI가 나와있다.
  • 이렇게 하여 로그인 버튼을 누르면 URL이 이동하여 카카오 로그인과 회원인증을 처리한다.

2. 카카오 로그인 동안 수행 내용을 컴포넌트에 작성하자

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
function KakaoLogin() {
const location = useLocation();
const navigate = useNavigate();
const KAKAO_CODE = location.search.split("=")[1];

const getKakaoToken = () => {
fetch(`https://kauth.kakao.com/oauth/token`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
},
body: `grant_type=authorization_code&client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}&code=${AUTHORIZE_CODE}`,
})
.then((res) => res.json())
.then((data) => {
if (data.access_token) {
localStorage.setItem("kakao_token", data.access_token);
} else {
navigate("/");
}
});
};

useEffect(() => {
if (!location.search) return;
getKakaoToken();
}, []);
}
1
2
3
POST /oauth/token HTTP/1.1
Host: kauth.kakao.com
Content-type: application/x-www-form-urlencoded;charset=utf-8
1
2
3
4
5
6
7
<!-- Request -->
curl -v -X POST "https://kauth.kakao.com/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "client_id=${REST_API_KEY}" \
--data-urlencode "redirect_uri=${REDIRECT_URI}" \
-d "code=${AUTHORIZE_CODE}"
  • 토큰을 얻기 위해서는 POST 요청을 위 URL로 요청을 보내라고 알려주고 있다.
  • Request에서 나온대로 fetch 함수에 적어주면 된다.

-d로 나와있는 부분이 body부분에 넣어주면된다. “”로 구분된 것들은 &기호로 구분해준다.

  • 인가 코드를 사용하기 위해서 Redirect URI에서 localhost:3000/auth/callback/kakao?code= 뒷부분을 사용하기 위해 split 코드를 사용하였다.

URLSearchParams를 사용하여도 가능하다.

결과

결과

이렇게 코드를 짜면 localStorage에 토큰까지 저장되며 로그인이 성공한 것을 알 수 있다.

이제는 토큰을 가지고 로그인 했다고 처리를 해주면 된다.

소감

이번에 OAuth2.0 기반의 카카오 로그인을 연동해보았는데 처음 해봐서 신기하기도 하고 토큰을 받고 이후에 어떻게 로그인 유지와 로그아웃 처리를 해줘야하는지도 고민할 수 있는 시간이 있어서 배운 점이 많았다.

앞으로 OAuth 연동에 대해 두려움이 줄어든 것 같다.

댓글 공유

Markdown 파일 React에서 불러오기

이번 프로젝트에서 .md 파일로 저장된 데이터를 React 화면에 렌더링해야하는 작업이 필요했다.

그래서 찾아본 결과 마크다운 파일을 리액트에서 불러오기 편하도록 react-markdown 라이브러리를 찾았다.

설치

1
npm install react-markdown

사용법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import "./App.css";
import { useEffect, useState } from "react";
import { ReactMarkdown } from "react-markdown/lib/react-markdown";
import Markdown from "./assets/markdown.md";

function App() {
const [markdown, setMarkdown] = useState("");

useEffect(() => {
fetch(Markdown)
.then((res) => res.text())
.then((data) => setMarkdown(data));
}, [markdown]);
return (
<div className="App">
<ReactMarkdown children={markdown} />
</div>
);
}

export default App;
  • 마크다운 파일을 import 해온 뒤 useEffect를 사용하여 컴포넌트가 마운트 될 때, 지정된 경로로 마크다운 파일을 로드한다.

결과

마크다운 업로드 사진

  • 마크다운 형식에 맞춰서 HTML형식으로 변환까지 잘 되었다.

TroubleShooting

1. import type 오류

나는 vite를 사용하여 프로젝트를 시작하였다.

이 때, 마크다운 파일을 리액트에서 import 해오면 vite에서는 md 파일을 경로에서 가져온다는 것을 기본적으로 포함하고 있지 않기 때문에 경로에 빨간 밑줄(error)이 생긴다.

이를 해결하기 위해서는 TypeScript에게 md 파일을 경로에서 가져온다는 타입을 선언해줘야한다.

1
2
3
4
5
// d.ts
declare module "*.md" {
const content: string;
export default content;
}

2. vite md파일 형식 오류

markdown 오류

vite 에서는 md 파일을 다룰 적절한 플러그인을 설치하거나 만약 이것이 asset이라면, vite.config.ts안에서 **/*.md를 assetsInclude에 할당하라고 알려주고 있다.

1
2
3
4
5
6
7
8
9
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
assetsInclude: ["**/*.md"],
});
  • 나는 **/*.md를 assetsInclude에 할당하라는 것이 어떤 파일에서 해야하는지 몰라서 tsconfig.json에 넣어서 해결하는데 시간이 걸렸다.

댓글 공유

useEffect 초기 렌더링 스킵하기

종속성 배열에 요소에 의존하여 어떤 로직을 실행하고 싶을 때, useEffect를 사용한다.

그런데 useEffect는 무조건 처음에 렌더링 되기 전에 실행된다.

이번 프로젝트에서 Nickname 페이지에서 Nickname을 수정하고 Mypage로 이동하여 Modal을 띄워야하는 경우가 있었다.

처음에는 Mypage에서 useEffect로 종속성 배열에 user를 넣어서 user가 변경되었음을 감지하고 effect 함수가 실행되기를 바랬지만, 이는 두가지 문제점이 있었다.

문제점1. effect는 처음에 한번 무조건 실행된다.

Nickname이 변경되었을 때만 Modal을 띄워야 하는데, Mypage가 렌더링 될 때마다 모달이 뜨는 문제가 발생하였다.

이를 해결하기 위해서는 effect 함수의 초기 렌더링때는 스킵해줘야한다.

1
2
3
4
5
6
7
8
9
10
11
12
import { useEffect, useRef } from "react";

export function useDidUpdateEffect(fn, inputs) {
const didMountRef = useRef(false);

useEffect(() => {
if (didMountRef.current) {
return fn;
}
didMountRef.current = true;
}, inputs);
}

useRef를 사용하여 초기 렌더링 때 effect 함수가 실행되는 것을 스킵할 수 있다.

문제점2. user 상태를 종속성배열로 넣어도 변화를 감지하지 못한다.

Mypage 안의 useEffect는 결국 Mypage 컴포넌트가 불러와져서 렌더링 되기 이전에 실행되는 코드이다.

그러므로 Mypage가 렌더링 되기 이전에 Nickname 페이지에서 상태를 변경한 것을 감지할 수 없다는 것이다.

이를 망각한 체 useEffect에서 어떻게하면 전역 상태의 변경을 감지할 수 있을지에 대해 오랜 시간 고민하며 시간을 허비했다…

이를 해결하기 위해서는 전역 상태로 nickname 수정이 완료되었다는 상태(NicknameModal)을 갖도록 하여 간단하게 해결할 수 있었다.

댓글 공유

loco9939

author.bio


author.job