ㅇㅇㅈ Blog

프론트엔드 수행중

0%

Vite로 구성한 React Project에 eslint 설정하기

CRA와 다르게 vite는 eslint가 기본설치 되지 않으므로 필요 모듈 설치 및 config작성을 따로 해줘야 한다.

vite로 react proejct 생성

생각보다 간단하다

1
2
3
$ npm create vite@latest
또는
$ yarn create vite

명령어를 입력하고 나면 다음 이미지처럼 나오는데 키보드로 이동해 선택 해서 만들면 끝

Read more »

Programmers 모의고사 JavaScript

문제 설명

1
2
3
4
5
6
7
수포자는 수학을 포기한 사람의 준말입니다. 수포자 삼인방은 모의고사에 수학 문제를 전부 찍으려 합니다. 수포자는 1번 문제부터 마지막 문제까지 다음과 같이 찍습니다.

1번 수포자가 찍는 방식: 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...
2번 수포자가 찍는 방식: 2, 1, 2, 3, 2, 4, 2, 5, 2, 1, 2, 3, 2, 4, 2, 5, ...
3번 수포자가 찍는 방식: 3, 3, 1, 1, 2, 2, 4, 4, 5, 5, 3, 3, 1, 1, 2, 2, 4, 4, 5, 5, ...

1번 문제부터 마지막 문제까지의 정답이 순서대로 들은 배열 answers가 주어졌을 때, 가장 많은 문제를 맞힌 사람이 누구인지 배열에 담아 return 하도록 solution 함수를 작성해주세요.

제한 조건

  • 시험은 최대 10,000 문제로 구성되어있습니다.
  • 문제의 정답은 1, 2, 3, 4, 5중 하나입니다.
  • 가장 높은 점수를 받은 사람이 여럿일 경우, return하는 값을 오름차순 정렬해주세요.

입출력 예

answers return
[1,2,3,4,5] [1]
[1,3,2,4,2] [1,2,3]

내 풀이

  1. 반복되는 a,b,c의 수를 answers의 길이만큼 배열로 만들어서 저장한다.
  2. answers와 비교하여 맞는 정답수의 길이를 구한다.
  3. a,b,c 중 가장 큰 길이를 리턴한다.
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
const makeArray = (arr, length) => {
return [...Array(length)].map((_, i) => arr[i % arr.length]).flat()
}
const filterArrLength = (ast, ans) => ans.filter((x, i) => ans[i] === ast[i]).length

function solution(answers) {
const a = makeArray([1, 2, 3, 4, 5], answers.length)
const b = makeArray([2, 1, 2, 3, 2, 4, 2, 5], answers.length)
const c = makeArray([3, 3, 1, 1, 2, 2, 4, 4, 5, 5], answers.length)

const answerArr = [
filterArrLength(a, answers),
filterArrLength(b, answers),
filterArrLength(c, answers),
]

const maxCount = Math.max(...answerArr)
let answer = []
for (let i = 0; i < answerArr.length; i++) {
if (answerArr[i] === maxCount) {
answer.push(i + 1)
}
}
return answer
}

a,b,c의 배열 만드는 코드

1
2
3
const makeArray = (arr, length) => {
return [...Array(length)].map((_, i) => arr[i % arr.length]).flat()
}

arr이 [1, 2, 3, 4, 5]이고 length가 8이라면, […Array(length)]은 [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]를 만들게 되고, i(index)가 0일때 arr[0 % 5]는 0이므로 arr의 0번째 인덱스 1이 들어가게 된다.
다른건 필터하고 높은 값 구해서 answer에 push 해주는 것..

다른사람의 풀이를 보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function solution(answers) {
var answer = [];
var a1 = [1, 2, 3, 4, 5];
var a2 = [2, 1, 2, 3, 2, 4, 2, 5]
var a3 = [3, 3, 1, 1, 2, 2, 4, 4, 5, 5];

var a1c = answers.filter((a,i)=> a === a1[i%a1.length]).length;
var a2c = answers.filter((a,i)=> a === a2[i%a2.length]).length;
var a3c = answers.filter((a,i)=> a === a3[i%a3.length]).length;
var max = Math.max(a1c,a2c,a3c);

if (a1c === max) {answer.push(1)};
if (a2c === max) {answer.push(2)};
if (a3c === max) {answer.push(3)};


return answer;
}

간단, 나처럼 배열을 만드는 과정없이 바로 filter를 했다.

github actions secrets로 env파일 생성

vite로 구성한 react앱을 github actions로 빌드하고 aws에 배포하는 과정에서
unauthorized 에러가 계속 뜨길래 빌드된 js 파일을 찾아봤더니
env가 포함되지 않아 apikey를 찾지 못하는 에러가 발생하였다.

github secrets에 apikey를 저장해놓고 actions 과정에 env파일을 생성하고,
secrets에 저장해 둔 key를 삽입해 줄 수 있다.

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
name: deploy # 해당 workflow의 이름
on: # 언제 이 action 이 실행 되는지
push: # push를 했을때
branches:
- main # main 브랜치에

jobs: # action
deploy:
runs-on: ubuntu-latest # action이 실행되는 환경?
steps: # step 안에는 shell script를 작성할수도 있고 다른사람이 만들어 놓은 action을 사용 할 수도 있다
- uses: actions/checkout@v3
# GitHub Actions는 해당 프로젝트를 리눅스 환경에 checkout하고 나서 실행

- run: rm -rf node_modules && yarn install --frozen-lockfile
# 노드 모듈을 지우고 나서 새로 설치하라는 터미널 명령
# npm install로 작성해도 된다.

- name: Generate Environment Variables File for Production
run: |
echo "VITE_API_KEY=$VITE_API_KEY" >> .env.production
env:
VITE_API_KEY: ${{ secrets.VITE_API_KEY }}

- run: yarn build
# build

- name: deploy to s3 bucket
uses: jakejarvis/s3-sync-action@master # 다른 사람이 만들어 놓은 action을 가져와서 사용 할 수도 있다.
with:
args: --delete
# github에 환경 변수를 저장해 놓고 사용 할 수 있다.
# aws를 이용해서 배포했으니 aws에 접근할 수 있는 키가 필요하다.
env:
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: "ap-northeast-2"
SOURCE_DIR: "dist"
Read more »

aws s3 버킷 정책 에러

최근 만든 project를 aws에 배포할려고 하는데

계속 이 에러가 떳다..

검색도해보고 gpt한테도 물어봤는데

1
2
3
4
5
6
7
8
9
10
11
12
13
[
{
"Condition": {
"HttpErrorCodeReturnedEquals": "404"
},
"Redirect": {
"Protocol": "https",
"HostName": "<host>",
"ReplaceKeyPrefixWith": "<prefix>",
"HttpRedirectCode": "307"
}
}
]

이걸 적으라고 한다.. 저장은 되지만 403error 가 뜬다

고민좀하다가…

권한 탭에 있는 버킷 정책에 작성 해봤다..

그랬더니 잘 된다 🤣

프로젝트 Amazon으로 배포하기

원티드 세션에서 Amazon으로 서비스를 배포하고, Github Action으로 자동 배포하는거 까지 배운 내용을 정리한다.

Amazon s3 버킷 만들기

aws에서 s3 서비스를 검색하여 새로운 버킷을 만들어 준다.

버킷 이름을 적어주고
AWS리전은 아시아 태평양(서울)ap-northeast-2 로 선택을 해주었다.

다른 옵션은 딱히 건들지 않고,

퍼블릭 액세스 차단 설정을 풀어주지 않으면 웹페이지가 403뜨면서 접속 할 수가 없다.

버킷을 만들기를 하고 나면 리스트에 버킷이 생성 된다.

만든 버킷으로 들어가 속성탭 가장 아래에 정적 웹 사이트 호스팅에서 편집을 해준다.

인덱스 문서에서 index.html을 entry 파일로 지정해준다.

그리고 나서 권한 탭으로 들어가 버킷 정책을 작성 해준다.

1
2
3
4
5
6
7
8
9
10
11
12
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::deploy-test1/*"
}
]
}

Resource 부분에는 "arn:aws:s3:::<자신의 버킷이름>/*" 으로 작성 해줘야한다.

그리고 나서 객체 탭으로 들어가 빌드한 파일들을 올려주기만 하면 된다.

이러면 배포 자체는 끝난다.

aws에서 access key 발급

깃헙 action을 통해 자동배포를 할려면 aws에서 access_key를 발급받아야 한다.
사용자 탭에서 보안자격증명으로 들어가준다.

새 액세스 키를 만들어주면 되는데 주의할 점은 보안 액세스 키는 처음 생성할때만 확인 할 수 있으므로
키 파일을 다운로드 받아놓거나 다른 곳에 잘 적어놔야 한다.. 아니면 삭제하고 다시 만들어야 함!

Read more »

원티드 3차 과제 중 무한 스크롤을 구현해야했다

react-queryuseInfiniteQuery를 이용해서 만들었다
intersectionObserver를 이용해 감지요소가 화면에 들어오면 api 요청을 하는건 기존에 했던 방식과 비슷하다

1
2
3
4
5
6
7
8
9
10
11
const { data, isLoading, hasNextPage, fetchNextPage, isFetching } = useInfiniteQuery(
[`${movieListQueryKey}`, `${pathname}`],
({ pageParam = 1 }) => getMovieListApi(movieListQueryKey, pageParam),
{
getNextPageParam: (lastPage, _) => {
const maxPages = lastPage.total_pages
const nextPage = lastPage.page + 1
return nextPage <= maxPages ? nextPage : undefined
},
},
)

useQuery와 비슷한 리턴값을 갖는다.
hasNextPage는 boolean으로 다음 페이지가 있는지,
fetchNextPage는 말그대로 다음 페이지를 요청해주는 메소드,
isFetching은 현재 api 요청중인지를 boolean으로 리턴해준다

getNextPageParam의 리턴값이 다음 요청때 쓰일 param을 리턴 시키면 pageParam 에 들어가서 다음 요청 파라미터로 사용된다.

getNextPageParam의 lastPage를 콘솔로 찍으면 실제 데이터가 들어있다.
여기서 다음페이지를 요청할때 필요한건 page값이니 이걸 +1 시켜서 리턴해준다

data를 콘솔로 찍으면 pagePrams에는 page값이 들어있고
pages에 array로 데이터가 들어있다.

화면에 뿌려지기 위한 데이터는 results 배열에 들어있으므로 이중 배열이 된다

그냥 map을 두번 돌아서 화면에 렌더링 할 수도 있지만
flatMap이란 메소드를 이용해서 작업하였다

1
2
3
4
5
const movieResults = useMemo(() => {
const results = data?.pages.flatMap(page => page.results)
return results || null
}, [data])

flatMap은 말그대로 flatmap을 합쳐 놓은거다 각 엘리먼트에 map수행 후, 결과를
flat으로 평탄화 해준다.

  • private

    • 정의된 클래스 내에서만 접근 가능
    • 상속받는 클래스에서는 접근 불가
  • protected

    • private와 다른 점은 해당 클래스뿐만 아니라 확장하는 모든 클래스에서도 사용 가능
  • static

    • 정적 속성은 인스턴스에서 유효하지 않는다
    • this로 접근 불가, 클래스 이름으로 접근해야 함
1
2
3
4
5
6
7
8
9
abstract class Department {
static fiscalYear = 2020;

constructor(protected readonly id: string, public name: string) {
this.id = id;
console.log(this.fiscalYear); // 에러 발생 this는 Department 클래스를 기반으로 생성된 인스턴스를 참조하기 때문
console.log(Department.fiscalYear); // 올바른 접근
}
}
  • implements
    • 클래스가 특정 interface의 조건을 만족하는지 체크하기 위해 사용
    • 쉼표로 여러개의 interface를 implements 할 수 있다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface ValidUser {
user: string;
}

interface ediable {
eat: () => boolean;
}

class Pserson implements ValidUser, ediable {
user: string;
constructor(n: string) {
this.user = n;
}

eat() {}
}
  • type 확장
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Admin = {
// interface로 만들어도 된다
name: string;
privileges: string[];
};

type Employee = {
// interface로 만들어도 된다
name: string;
startData: Date;
};

type ElevatedEmployee = Admin & Employee;
interface ElevatedEmployee extends Admin, Employee {}
  • intersection
1
2
3
4
5
type Combinable = string | number;
type Numeric = number | boolean;

type Universal = Combinable & Numeric;
// Universal은 number type이 된다
  • type guard
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type UnknownEmployee = Employee | Admin;

function printEmployeeInformation(emp: UnknownEmployee) {
console.log("Name: " + emp.name);

console.log("Privileages" + emp.privileges); // Err 발생 Employee 타입에 privileges가 없기때문이다

if (typeof emp === "object") {
} // typeof emp는 object이기때문에 typeof키워드로 타입가드를 할 수 없다

if (typeof emp === "Employee") {
} // 이 if문은 JS에서 런타임때 동작하기때문에 type인 Employee로 조건을 구할 수 없다

if ("privileges" in emp) {
// privileges가 emp의 속성인지 in으로 확인 할 수 있다
console.log("Privileages" + emp.privileges);
// 정상 동작
}
}
Read more »

tsconfig.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"compilerOptions": {
"target": "es5", // 어떤 버전의 js로 컴파일 할건지
"module":"commonjs",
"sourceMap": true , // browser에서 ts파일을 직접 디버깅 할 수 있게 sourcemap파일 생성
"outDir": "./dist", // 컴파일했을때 결과물이 저장될 디렉토리
"rootDir":"/.src", // 컴파일될 루트 디렉토리 지정
"removeComments":true, // 컴파일 시 주석 삭제
"noEmitOnError":false // default=false -> true일때 ts에 에러가 있을시 컴파일을 멈추고 js파일을 생성하지 않는다.
"strictNullChecks":true // null 값을 잠재적으로 가질 수 있는 값에 에러표시


}
}

outDir 옵션을 ./dist로 하고 컴파일 하면 폴더 디렉토리까지 그대로 dist에 생성 된다

주의 할점

rootDir옵션을 사용하지 않고 src 디렉토리 외부에 다른 ts파일이 있다면
컴파일 했을시 src 폴더 자체도 컴파일에 포함되어 dist에 들어가게 된다.

compilerOptionsrootDir 옵션을 만들어주고 다시 컴파일하면

1
2
3
4
5
6
{
"compilerOptions": {
...
"rootDir":"./src"
}
}

src 폴더 내부의 파일만 컴파일 되어 dist에 들어 가게 된다

map()메소드를 이용해서 컴포넌트를 렌더링 하는데
이런 에러가 떴다.

잉 이게 무슨 말이야!

key 값은 넣어줬고….
type이랑 props가 없다는게 무슨 말이야!?

검색을 했는데 내가 원하는 답을 찾기가 어려웠다

의외로 단순한 문제였는데
<></> 프래그먼트로 감싸주니 해결되었다.

jsx에서 map을 이용해서 컴포넌트를 렌더링 할때도 프래그먼트로 감싸주었었나?..
헷갈린다.

typescript 공부를 한다고 ts로만 작성을 하는중인데 사실 이게 맞게 하는건지 잘 모르겠다

react-firebase 로 로그인 하기

파이널프로젝트가 끝나고 조원끼리 사이드 프로젝트를 하기로 했다.
firebase를 이용하기로 해서 공부를 했다.

react를 설치해주고
firebase도 따로 설치 해준다

1
$ npm i firebase

firebase 사이트에서 프로젝트를 추가 해준다

프로젝트 만들기가 끝나면 해당 프로젝트로 들어가서
프로젝트 설정에 가서 web으로 앱을 만들어 준다

앱등록이 되면

firebase를 설치하라는 설명과 함께 firebaseConfig 가 나온다

이 config를 react에 붙여 넣어줄거다

이메일로 회원가입을 할 수 있게 설정 해준다

src 하위에 firebase 디렉토리를 만들어주고 index.ts를 생성 해준다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/firebase/index.js

import { initializeApp } from "firebase/app";
import { getFirestore } from "@firebase/firestore";

const firebaseConfig = {
apiKey: "AIzaSyAxURV7vxqnqWyii5jsD4tEwMX2ulwX0lQ",
authDomain: "fir-chat-app-b793f.firebaseapp.com",
projectId: "fir-chat-app-b793f",
storageBucket: "fir-chat-app-b793f.appspot.com",
messagingSenderId: "881524092288",
appId: "1:881524092288:web:384226bf067529d8f0a565",
measurementId: "G-9J8RV4TTR0",
};

const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);

그럼 firebase 설정 끝 생각보다 간단하다.

회원가입

Resister.tsx를 만들어주고
간단하게 input과 회원가입 버튼을 만들어 준다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from "react";

const Resister = () => {
return (
<>
<form>
ID <input type="text" name="id" />
PW <input type="text" name="pw" />
<button>회원가입</button>
</form>
</>
);
};

export default Resister;

submit 이벤트를 만들어서 input의 밸류를 받아온다

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

const Resister = () => {
const handleSubmit = (e:React.FormEvent<HTMLFormElement>)=>{
e.preventDefault();
const data = new FormData(e.currentTarget);
const id = data.get("id");
const pw = data.get("pw");
}
return (
<>
<form onSubmit={handleSubmit}>
ID <input type="text" name="id" />
PW <input type="text" name="pw" />
<button>회원가입</button>
</form>
</>
);
};

firebase에서 제공하는 메서드인 createUserWithEmailAndPassword() 이용해서 회원가입을 할 수 있다.

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
import React from "react";
import "../firebase";
import { createUserWithEmailAndPassword, getAuth } from "firebase/auth";

type Iidpw = FormDataEntryValue | null;

const Resister = () => {

const createUser = async (id: Iidpw, pw: Iidpw) => {
try {
const res = await createUserWithEmailAndPassword(
getAuth(),
id!.toString(),
pw!.toString()
);
} catch (e) {
console.log(e);
}
};

const handleResister = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = new FormData(e.currentTarget);
const id = data.get("id");
const pw = data.get("pw");

createUser(id, pw);
};

return (
<>
<form onSubmit={handleResister}>
ID <input type="text" name="id" />
PW <input type="text" name="pw" />
<button>회원가입</button>
</form>
</>
);
};

export default Resister;

createUser()라는 함수를 만들어서 submit이벤트 안에서 idpw를 인수로 넘겨 주면서 실행한다.

createUserWithEmailAndPassword()는 3개의 매개변수를 받는다.
auth에는 firebase에서 제공하는 getAuth()를 넣어주면된다
두 번째 , 세 번째에는 id와 pw를 넣어준다

createUserWithEmailAndPassword()는 promise를 반환하기 때문에
비동기 처리를 해줘야한다.

createUserWithEmailAndPassword()에 id와 pw는 type이 string이어야 하는데,
formData로 id와 pw를 가져왔더니 데이터 type이 string이 아니여서 string으로 타입변환을 해주어야 한다.

그런데도 계속 에러가 떠서 봤더니 type에 null도 있어서 그런거 같다.

1
2
3
4
5
6
const res = await createUserWithEmailAndPassword(
getAuth(),
id!.toString(),
pw!.toString()
);

id와 pw에 !를 붙여 null, undefined가 아니라고 단언 해준다!

Non-null assertion operator란?

접미에 붙는 느낌표(!) 연산자인 단언 연산자는 해당 피연산자가 null, undeifined가 아니라고 단언해준다.

아이디와 비밀번호를 치고 회원가입을 하면 firebase에서 회원가입이 된걸 확인 할 수 있다.
router를 이용해서 redirect를 시켜 주기만 하면된다!