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

댓글 공유

문제

Finn은 요즘 수학공부에 빠져 있습니다. 수학 공부를 하던 Finn은 자연수 n연속한 자연수들로 표현 하는 방법이 여러개라는 사실을 알게 되었습니다. 예를들어 15는 다음과 같이 4가지로 표현 할 수 있습니다.

1
2
3
4
1 + 2 + 3 + 4 + 5 = 15
4 + 5 + 6 = 15
7 + 8 = 15
15 = 15

자연수 n이 매개변수로 주어질 때, 연속된 자연수들로 n을 표현하는 방법의 수를 return하는 solution를 완성해주세요.

제한사항

  • n은 10,000 이하의 자연수 입니다.

풀이

1
2
3
4
5
6
7
8
9
10
11
12
13
def solution(n):
q = [1]
answer, i, total = 1, 1, 1
mid = int((n+1) / 2) # 절반보다 큰 값부터는 값이 없으니 절반까지 반복문 실행
while i < mid:
i += 1
total += i
q.append(i)
while total > n:
total -= q.pop(0)
if total == n:
answer += 1
return answer
  • 원소가 1만 있는 큐(리스트)를 만든다.
  • 주어진 수의 절반까지 반복한다.
  • 차례로 수를 더할 때마다 queue에 수를 추가한다.
    • 더한 값이 원하는 수보다 큰 동안, 큐에서 수를 하나씩 꺼내어 합에서 뺸다.
    • 더한 값이 원하는 수이면 정답 개수를 하나 늘린다.

댓글 공유

문제

양의 정수 n이 주어질 때 1부터 n까지의 십진수를 이진수로 출력하라.

Generatre Binary Numbers

1. 내장함수 풀기

1
2
3
def generate_binary(n):
for i in range(1, n+1):
print(bin(i)[2:]) # 이진수는 0b가 붙으므로 슬라이싱으로 제거한다.

2. 큐를 사용하여 풀기

문제를 분석해보면, 2의 이진수는 10이고 3의 이진수는 11이다.

이는 1에다가 01을 붙인 것이다.

4의 이진수는 100이고 5의 이진수는 101이다. 이는 2의 이진수 10에다가 01을 추가한 것이다.

앞서 사용한 숫자에 01을 붙인 이진수가 나중에 사용된다.

  1. 큐를 하나 생성하고 1을 삽입한다.

  2. n만큼 반복한다.

    1. 큐에서 꺼낸 값을 i에 저장
    2. i에 01을 붙인 이진수를 큐에 삽입
    3. i를 출력
1
2
3
4
5
6
7
8
9
def generate_binary_queue(n):
q = Queue() # 직접 구현한 Queue 클래스
q.enqueue('1')

for _ in range(n):
i = q.dequeue()
q.enqueue(i+'0')
q.enqueue(i+'1')
print(i)

댓글 공유

2. queue로 풀기

josephus

1
2
3
4
5
6
7
8
9
10
11
def josephus(n,k):
res = []
q = Queue() # 직접 구현한 Queue 클래스
for i in range(n):
q.enqueue(i+1)
while q.front.next: # 끝에서 2번째 원소까지 반복
for _ in range(k-1):
q.enqueue(q.dequeue())
res.append(q.dequeue())
res.append(q.dequeue()) # 마지막 원소 출력
return res

댓글 공유

문제

1번부터 N 번까지 N명의 사람이 원을 이루면서 앉아있고, 양의 정수 K(≤ N)가 주어진다. 이제 순서대로 K 번째 사람을 제거한다. 한 사람이 제거되면 남은 사람들로 이루어진 원을 따라 이 과정을 계속해 나간다. 이 과정은 N명의 사람이 모두 제거될 때까지 계속된다. 원에서 사람들이 제거되는 순서를 (N, K)-요세푸스 순열이라고 한다. 예를 들어 (7, 3)-요세푸스 순열은 <3, 6, 2, 7, 5, 1, 4>이다.

N과 K가 주어지면 (N, K)-요세푸스 순열을 구하는 프로그램을 작성하시오.

예시

  • 입력: 7, 3
  • 출력: [3, 6, 2, 7, 5, 1, 4]

1. list로 풀기

예시대로 3번째 마다 사람을 제거하려면, 처음에는 시작점이니 2번만 인덱스를 이동하고 제거한 후 이 후에는 3번 인덱스를 이동해서 제거한다.

첫 시작을 0이 아닌 -1로 시작하는 점에 유의하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def josephus(n,k):
res = []
line = [1 for _ in range(n)] # 값이 1이면 제거 대상, 0이면 제거 완료
i = -1

for _ in range(n-1):
count = 0
while count < k:
i = (i + 1) % n # i는 0~6까지 순회해야하므로 나머지로 계산한다.
if line[i]: # line의 원소가 0이 아니면...
count += 1
line[i] = 0
res.append(i+1)

res.append(line.index(1)+1)
return res
  1. line 배열을 1로 초기화 해준다. 1은 아직 제거안된 요소이고 0은 제거된 요소로 처리
  2. 마지막 1개가 남을 때 까지 반복하므로 n-1회 반복한다.
  3. count 변수는 제거되지 않은 원소를 지나온 순서를 카운팅하기 위해 사용한다.
  4. 0 ~ 6까지 index가 순회해야하므로, n으로 나눈 나머지 값을 이용한다.
  5. i를 순회하면서 line의 요소가 0이 아니면, 즉 아직 제거되지 않았으면 순서를 카운팅 해준다.
  6. 순서를 카운팅 해서 k번째 순서에 도달하면, 반복을 멈추고 해당 index 요소를 0으로 바꾼다. (요소 제거)
  7. 해당 index는 0부터 시작했으므로, 결과 배열에 넣어줄 땐 +1을 해준다. 왜냐하면 문제에선 1부터 시작한다 했으니 때문이다.
  8. 이렇게 n-1번 반복하게되면 line의 요소는 1개 남는다. 이 때는 index 메서드를 사용하여 1인 요소의 index를 구한 뒤 결과배열에 추가한다. 이 때도 + 1 을 해줘야한다.

댓글 공유

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는 왠만한 프로젝트에서 자주 사용되기 때문에 자동적으로 지원해주는 라이브러리를 사용하자.

댓글 공유

loco9939

author.bio


author.job