Warning: A Component is changing an uncontrolled …

input 태그의 value와 onChange 프로퍼티를 통해서 제어 컴포넌트로 사용을 하고 있었는데, 갑자기 위와 같은 에러가 발생하였다.

디버깅을 해보니, input 태그의 value가 최초에 undefined 또는 null로 할당되었다가 이후에 값이 할당되면서 제어되지 않은 상태에서 제어되는 상태로 전환되는 경우에 발생하는 에러이다.

해결방법

1. 입력 요소를 제어되는 컴포넌트로 유지하는 경우

state 상태를 초기화할 때, undefined, null 대신 초기값을 설정한다.

1
const [state, setState] = useState("");
  • 초기값을 빈 문자여롤 둔다.

2. 입력 요소를 비제어 컴포넌트로 유지하는 경우

1
<input defaultValue="default" ref={inputRef} />
  • 비제어 컴포넌트로 사용하려면 value, onChange 대신 defaultValue, ref를 사용하여 입력 요소에 접근할 수 있다.

댓글 공유

React Day Picker 사용기

설치

1
npm install react-day-picker date-fns

예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react';

import { format } from 'date-fns';
import { DayPicker } from 'react-day-picker';
import 'react-day-picker/dist/style.css'

export default function Example() {
const [selected, setSelected] = React.useState<Date>();

let footer = <p>Please pick a day.</p>;
if (selected) {
footer = <p>You picked {format(selected, 'PP')}.</p>;
}
return (
<DayPicker
mode="single"
selected={selected}
onSelect={setSelected}
footer={footer}
/>
);
}
  • toDate 프로퍼티로 지정한 날짜까지만 노출되도록 할 수 있다.
  • class를 주어서 선택 불가능하도록 스타일링을 줄 수 도 있다.
  • React state를 사용하여 상태를 다루기 용이하다.

댓글 공유

커스텀 훅 사용 예시

카테고리 Daily

오늘은 회사에서 실전투자 API 개편사항을 반영하기 위해 기존 API를 수정하는 작업을 하였다.

그러던 중, 각 컴포넌트에서 동일한 백엔드 API 로직을 발견하고 이를 커스텀 훅으로 빼내어 관리하면 유지보수성 측면과 가독성 측면에서 개선될 것이라고 판단하여 적용해보았다.

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
// 기존 코드
const checkUserStatus = async () => {
const response = await tradeInstanceAPIWithToken.post("/url", {
requestBody
});

if (response.status === 200) {
... // 응답 성공시 코드
} else if (response.status === 403) {
... // 403일 때, 실행할 코드
}
};

const [accountLoading, setAccountLoading] = useState<boolean>(false);

const getUserAccount = async () => {
setAccountLoading(true);
await tradeInstanceAPI.post("/url2").then((res) => {
if (res.status === 200) {
... // 응답 성공 시 코드
setAccountLoading(false)
}
});
};

useLayoutEffect(() => {
if (subMenu === "accounts") {
// subMenu가 accounts 일 때,
getUserAccount();
} else {
// subMenu가 myport, trading일 때
checkUserStatus();
}
}, [subMenu]);
  • checkUserStatus, getUserAccount 함수를 useLayoutEffect 훅에서 호출한다.
  • 해당 백엔드 요청 API 코드는 다룬 곳에서도 쓰이기에 함수로 빼내면 유지보수성을 높일 수 있다.
  • 가독성 측면에서도 굳이 요청 로직을 다 볼 필요가 없다.

개선된 코드

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
// useCheckUserStatus.ts
import { useState } from "react";
import { tradeInstanceAPIWithToken } from "your-api-instance";

const useCheckUserStatus = () => {
// 백엔드 API 응답과 관련된 state들
const [errorType, setErrorType] = useState<number | string | null>(null);
...

const checkUserStatus = async () => {
const response = await tradeInstanceAPIWithToken.post("/url", {
requestBody
});

if (response.status === 200) {
... // 응답 성공시 코드
} else if (response.status === 403) {
... // 403일 때, 실행할 코드
}
};

return {
errorType,
...,
checkUserStatus,
};
};

export default useCheckUserStatus;
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
// useGetUserAccount.ts
import { useState } from "react";
import { tradeInstanceAPI } from "your-api-instance";

const useGetUserAccount = () => {
const [userAccounts, setUserAccounts] = useState<any[]>([]);
const [accountLoading, setAccountLoading] = useState<boolean>(false);

const getUserAccount = async () => {
setAccountLoading(true);
await tradeInstanceAPI.post("/trade/get_accounts").then((res) => {
if (res.status === 200) {
const { accounts } = res.data;
setUserAccounts(accounts);
setAccountLoading(false);
}
});
};

return {
userAccounts,
accountLoading,
getUserAccount,
};
};

export default useGetUserAccount;
  • 백엔드 로직의 로딩 상태까지 캡슐화 할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Main.tsx
import React, { useLayoutEffect } from 'react';
import useCheckUserStatus from './useCheckUserStatus';
import useGetUserAccount from './useGetUserAccount';

const Main = ({ user, license, isExpiredUser, subMenu }: any) => {
const { errorType, ..., checkUserStatus } = useCheckUserStat();
const { userAccounts, accountLoading, getUserAccount } = useGetUserAccount();

useLayoutEffect(() => {
if (subMenu === "accounts") {
getUserAccount();
} else {
checkUserStatus();
}
}, [subMenu]);

return (
// 여기에 컴포넌트의 JSX를 반환
);
};

export default Main;
  • 훨씬 코드가 간결해지고 가독성도 높아져서 추후에 유지보수할 때 시간을 많이 절약할 수 있을 것 같다.
  • 앞으로 서비스의 코드를 이런식으로 분리해보는 연습을 해봐야겠다.

댓글 공유

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function calculateDDay(targetDateStr) {
// 현재 날짜와 시간을 가져옵니다.
const now = new Date();
// 현재 날짜에 시간을 00:00:00으로 설정하여 날짜만 비교하게 합니다.
now.setHours(0, 0, 0, 0);

// 목표 날짜를 Date 객체로 변환합니다.
const targetDate = new Date(targetDateStr);
targetDate.setHours(0, 0, 0, 0);

// 두 날짜의 차이를 밀리초 단위로 계산한 후 일(day) 단위로 변환합니다.
const diffMilliseconds = targetDate.getTime() - now.getTime();
const diffDays = diffMilliseconds / (1000 * 60 * 60 * 24);

return Math.ceil(diffDays); // 올림처리하여 D-Day를 계산합니다.
}

이번에 사용권 만료 기간을 D-Day로 표시하기 위해 JavaScript로 D-Day를 계산해주는 함수를 구현해보았다.

댓글 공유

vscode ssh 익스텐션을 사용하다가 오래간만에 사용해서 방법을 까먹어서 기록한다.

  1. 터미널을 켜고 루트 경로에 가면 .ssh폴더에 붙여넣는다.
  2. vscode를 켜고 ssh remote extension을 설치하고 config 파일에 다음과 같이 명시한다.
1
2
3
4
5
// config
Host HostName
HostName "HostName 경로"
User ubuntu
IdentityFile /Users/.ssh/myPen.pem
  1. pem 키를 처음 등록하면 권한을 줘야하므로, 터미널로 들어가서 .ssh 폴더에서 다음 명령어를 실행한다.
1
chmod 400 <your>.pem

댓글 공유

ES6 import 클린 코드

카테고리 Daily
1
2
3
4
import { colors } from "../../../themes";
import Button from "../../../components/Button";
import MyImage from "../../../assets/images/my-image.png";
import { globalStyles } from "../../../styles/globalStyles";

위와 같은 import 코드를 깔끔하게 하기 위한 3가지 방법에 대해 알아보자.

1. Barrel 패턴

1
2
3
4
5
6
7
8
9
10
11
12
// folder structure
components;
--Accordions.js;
--Button.js;
--index.js;

// index.js
export { default as Accordion } from "./Accordion";
export { default as Button } from "./Button";

// imports
import { Accordion, Button } from "components";
  • index.js 파일에 담아서 export를 한곳에서 내보낸다.

2. Aliases 사용

  • 가독성을 높이는 짧은 경로
  • 파일을 옮기더라도, import는 바뀌지 않는다.
  • snippets을 사용하면 더 쉽다.
1
2
3
4
5
6
7
8
9
10
11
12
// babel.config.js
plugins: [
"module-resolver",
{
alias: {
"@internals/assets": "./src/assets",
"@internals/components": "./src/components",
"@internals/hooks": "./src/hooks",
...
},
},
];
  • babel을 사용할 때는 위와 같이 alias를 지정할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// vite.config.js

import { fileURLToPath, URL } from "node:url";

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});
  • vite를 사용하면 위와 같이 사용할 수 있다.
1
2
3
4
5
6
7
// tsconfig.json
"paths": {
"@internals/assets/*": ["./src/assets/*"],
"@internals/components": ["./src/components/index.ts"],
...

}
  • typescript를 사용하면 위와 같이 사용할 수 있다.

3. import 순서 지정

prettier-plugin-sort-import

prettier는 왠만한 프로젝트에서 자주 사용되기 때문에 자동적으로 지원해주는 라이브러리를 사용하자.

댓글 공유

컨테이너와 도커

카테고리 Daily

컨테이너

애플리케이션이 한 컴퓨팅 환경에서 다른 컴퓨팅 환경으로 빠르고 안정적으로 실행하기 위해 코드와 모든 종속성을 패키징하는 소프트웨어의 표준 단위

도커

컨테이너에 필요한 거의 모든 기능응 제공하는 플랫폼

  1. 도커 파일에 애플리케이션 구동에 필요한 환경설정 절차 작성 후 빌드
  2. 도커 이미지(컨테이너 실행에 필요한 데이터를 포한한 불변한 상태값) 생성되어 실행
    1. 하나의 이미지로 여러 컨테이너 생성가능
  3. 도커 컨테이너 생성되고 도커 컨테이너에 설정된 프로그램, 데이터 등이 컴퓨팅환경과 연결되어 동작

댓글 공유

Debounce

이벤트가 연달아 발생할 때, 제일 처음 또는 마지막 이벤트일 때만 함수를 호출하는 방법

만약 키보드 이벤트가 발생할 때마다 API를 요청한다고 가정해보자.

“감”이라는 글자를 입력하는데 “ㄱ”, “가”, “감” 3번의 이벤트가 발생하게됩니다. 이러면 불필요한 이벤트까지 API 요청에 포함시키면 낭비이므로 이를 방지하기 위해 디바운스를 사용합니다.

1
2
3
4
5
6
7
8
9
var timer;
document.querySelector("#input").addEventListener("input", function (e) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function () {
console.log("여기에 ajax 요청", e.target.value);
}, 200);
});
  • 주로 키보드 입력 이벤트에 사용

Throttle

마지막 이벤트가 발생한 후 일정 시간이 지나기 전에 다시 호출되지 않도록 막는 방법

만약 스크롤이벤트가 발생했을 때, 처음에 스크롤 이벤트가 발생할 때, 함수를 호출하고 몇초동안은 이벤트가 발생해도 함수를 호출시키지 않는 방법이다.

1
2
3
4
5
6
7
8
9
var timer;
document.querySelector("#input").addEventListener("input", function (e) {
if (!timer) {
timer = setTimeout(function () {
timer = null;
console.log("여기에 ajax 요청", e.target.value);
}, 200);
}
});
  • 주로 스크롤 이벤트에 사용

위와 같이 직접 구현하는 방법보다는 예외 사항을 처리하지 못할 경우도 있기때문에, _.debounce, _.throttle을 사용한다.

요즘은 토스에서도 해당 라이브러리를 지원하니 관심이 있으면 사용해보자.

토스 디바운스 라이브러리 바로가기

댓글 공유

var setTimeout Quiz

카테고리 Daily
1
2
3
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 1000);
}

다음 코드의 결과는??

1
2
3
4
5
6
// 정답
5;
5;
5;
5;
5;

이유

var 키워드로 선언한 변수는 함수 레벨 스코프를 갖는다. for문 코드 블록에서는 전역 변수로 선언되었기 때문에, 변수 i 값이 갱신된다.

setTimeout 함수는 비동기 처리 방식으로 실행된다.

setTimeout 함수는 Web API로 이동하여 타이머가 만료되면 Task Queue로 이동한다.

Task Queue에서 Call Stack이 비워질 때 까지 대기한다.

대기하는 동안, 다음 for 문이 돌고 있으므로, setTimeout 함수가 Task Queue에서 실행 컨텍스트가 비워질 때 까지 계속 대기한다.

이러한 이유로 i가 5가 될 때, Call Stack이 비워지므로 그때서야 이벤트 루프에 의해서 console 창에 출력되는 i 값이 5이므로 5가 5번 찍히게 된다.

해결방법

1. 블록 레벨 스코프

1
2
3
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 1000);
}

2. 즉시실행함수

1
2
3
4
5
for (var i = 0; i < 5; i++) {
(function (param) {
setTimeout(() => console.log(param), 1000);
})(i);
}

setTimeout 함수를 즉시실행함수로 감싸서 함수 레벨 스코프를 갖도록 해준 뒤 즉시실행함수의 인수로 i를 전달해주고 즉시실행함수 내부의 함수에 파라미터로 해당 인수를 어디서 참조할지 설정해주면 된다. 위 예제에서는 콘솔 로그의 인수로 파라미터를 전달해줘야 할 것이다.

댓글 공유

flex 박스 반응형 팁

카테고리 Daily

flex-grow와 flex-basis 사용하여 반응형 만들기

1
2
3
4
5
6
7
8
9
10
11
<form>
<label class="name" for="name-field">
Name:
<input id="name-field" />
</label>
<label class="email" for="email-field">
Email:
<input id="email-field" type="email" />
</label>
<button>Submit</button>
</form>
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
form {
padding: 8px;
border: 1px solid hsl(0deg 0% 50%);

/* display 속성 */
display: flex;
align-items: flex-end;
flex-wrap: wrap;
gap: 8px;
}

label {
font-weight: 500;
}
input {
display: block;
width: 100%;
height: 2.5rem;
margin-top: 4px;
}
button {
height: 2.5rem;

/* display 속성 */
flex-grow: 1;
flex-basis: 70px;
}

.name {
/* display 속성 */
flex-grow: 1;
flex-basis: 120px;
}
.email {
/* display 속성 */
flex-grow: 3;
flex-basis: 170px;
}
  • flex-grow는 flex 아이템이 컨테이너 안ㄴ에서 다른 아이템들에 비해 얼마나 많은 여유공간을 차지할 것인지 결정하는 값이다.
  • 위에서는 flex-grow가 총 5이니, 1/5,1/5,3/5씩 차지하게 된다.
  • flex-basis는 flex 아이템의 초기 크기를 결정한다.
  • flex-grow가 계산되기 이전에 아이템이 어느정도 크기를 가져야하는지를 정의한다.

댓글 공유

loco9939

author.bio


author.job