📌 타입스크립트란?

JavaScript는 동적 타입만을 제공하여 예측하기 어려운 타입변환으로 디버깅이 어려워지는 문제점이 있어 이를 해결하고자 TypeScript가 탄생하였다.

TypeScript는 정적 타입 시스템을 사용하여 코드가 실행되기 전에 코드에 대하여 예측해준다.

계속 읽기

📌 미션

  • Link 역할로써 span태그, img태그 확인
  • tabindex=”0”를 포함
  • img태그는 alt 속성으로 접근 가능한 이름 정의

🐒 문제

🤿 컴포넌트 props “as”를 img로 주면, img 태그로 렌더링 해주기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import styled from "styled-components";
import { goToLink } from "../../utils";

export function Link({ as = ComponentName, href, children }) {
const StyledLink = styled(`${as}`)`
... styles
`;
return (
<>
<StyledLink
tabIndex="0"
role="link"
onClick={(e) => goToLink(e, href)}
onKeyDown={(e) => goToLink(e, href)}
>
{children}
</StyledLink>
</>
);
}

Link.defaultProps = {
as: "span",
};
  • Link 컴포넌트 안에다가 styled-component를 생성하고 as props를 전달해주었다.

  • 위와 같이 할 경우, as props에 img 태그가 들어오게 된다면 컴포넌트 명명 규칙에 어긋난다고 나온다.

    이를 해결하기 위해 조건부 렌더링을 해주었다.

    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
    import styled from "styled-components";
    import { goToLink } from "../../utils";

    export function Link({ as = ComponentName, href, children }) {
    const StyledLink = styled(`${as}`)`
    ... styles
    `;
    return (
    <>
    {as === "img" ? (
    <img
    tabIndex="0"
    role="link"
    onClick={(e) => goToLink(e, href)}
    onKeyDown={(e) => goToLink(e, href)}
    src="./gitprofile.jpeg"
    alt="W3C Website"
    />
    ) : (
    <StyledLink
    tabIndex="0"
    role="link"
    onClick={(e) => goToLink(e, href)}
    onKeyDown={(e) => goToLink(e, href)}
    >
    {children}
    </StyledLink>
    )}
    </>
    );
    }

    Link.defaultProps = {
    as: "span",
    };

    🐥 img가 업로드 되지 않는 문제

    • img 업로드 문제 해결

    문제의 원인은 npm 명령어로 webpack server만 실행시켰는데, webpack config.js 파일에 static 속성값을 [build]로 주었기 때문에 정적 이미지 파일을 불러오기 위해서는 build 폴더에서 assets을 찾는다.

    그러므로 npm run build 명령어로 build 폴더를 생성한 후 그 안에다가 img를 넣어주고 경로를 설정해주어 해결하였다.

    1
    2
    3
    4
    5
    6
    <Link
    as="img"
    alt="test이미지"
    src="./assets/img/gitprofile.jpeg"
    href="https://www.w3.org/WAI/ARIA/apg/example-index/link/link.html"
    />

    🐥 aria-label 속성 있을 때, 조건부로 css 추가 (삭제)

    이 속성을 삭제한 이유는 span 태그를 사용하여 Link 컴포넌트를 구현하였을 때, role="link"로 해주었기 때문에 aria-label 속성을 굳이 해주지 않아도 되므로 삭제하였다.

🏓 회고

  • styled-component를 사용하면서 어떻게 props를 받아서 App에서 렌더링 시켜야할지 헷갈렸었는데 직접 해보니 이해가 잘되었다.
  • 수업 시간에 배웠던 조건부 렌더링을 실제로 사용하면서 익히니 더욱 이해가 잘되었다. 역시 무언가를 만들어보면서 배우는 것이 힘들지만 더 도움되는 것 같다.

댓글 공유

📌 미션 - 로또

🔍 진행 방식

  • 미션은 기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항 세 가지로 구성되어 있다.
  • 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다.
  • 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다.

🚀 기능 요구 사항

로또 게임 기능을 구현해야 한다. 로또 게임은 아래와 같은 규칙으로 진행된다.

1
2
3
4
- 로또 번호의 숫자 범위는 1~45까지이다.
- 1개의 로또를 발행할 때 중복되지 않는 6개의 숫자를 뽑는다.
- 당첨 번호 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑는다.
- 당첨은 1등부터 5등까지 있다. 당첨 기준과 금액은 아래와 같다. - 1등: 6개 번호 일치 / 2,000,000,000원 - 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원 - 3등: 5개 번호 일치 / 1,500,000원 - 4등: 4개 번호 일치 / 50,000원 - 5등: 3개 번호 일치 / 5,000원
  • 로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다.
  • 로또 1장의 가격은 1,000원이다.
  • 당첨 번호와 보너스 번호를 입력받는다.
  • 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력하고 로또 게임을 종료한다.
  • 사용자가 잘못된 값을 입력할 경우 throw문을 사용해 예외를 발생시키고, “[ERROR]”로 시작하는 에러 메시지를 출력 후 종료한다.

입출력 요구 사항

입력

  • 로또 구입 금액을 입력 받는다. 구입 금액은 1,000원 단위로 입력 받으며 1,000원으로 나누어 떨어지지 않는 경우 예외 처리한다.
1
14000
  • 당첨 번호를 입력 받는다. 번호는 쉼표(,)를 기준으로 구분한다.
1
1,2,3,4,5,6
  • 보너스 번호를 입력 받는다.
1
7

출력

  • 발행한 로또 수량 및 번호를 출력한다. 로또 번호는 오름차순으로 정렬하여 보여준다.
1
2
3
4
5
6
7
8
9
8개를 구매했습니다.
[8, 21, 23, 41, 42, 43]
[3, 5, 11, 16, 32, 38]
[7, 11, 16, 35, 36, 44]
[1, 8, 11, 31, 41, 42]
[13, 14, 16, 38, 42, 45]
[7, 11, 30, 40, 42, 43]
[2, 13, 22, 32, 38, 45]
[1, 3, 5, 14, 22, 45]
  • 당첨 내역을 출력한다.
1
2
3
4
5
3개 일치 (5,000원) - 1개
4개 일치 (50,000원) - 0개
5개 일치 (1,500,000원) - 0개
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
6개 일치 (2,000,000,000원) - 0개
  • 수익률은 소수점 둘째 자리에서 반올림한다. (ex. 100.0%, 51.5%, 1,000,000.0%)
1
총 수익률은 62.5%입니다.
  • 예외 상황 시 에러 문구를 출력해야 한다. 단, 에러 문구는 “[ERROR]”로 시작해야 한다.
1
[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.

실행 결과 예시

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
구입금액을 입력해 주세요.
8000

8개를 구매했습니다.
[8, 21, 23, 41, 42, 43]
[3, 5, 11, 16, 32, 38]
[7, 11, 16, 35, 36, 44]
[1, 8, 11, 31, 41, 42]
[13, 14, 16, 38, 42, 45]
[7, 11, 30, 40, 42, 43]
[2, 13, 22, 32, 38, 45]
[1, 3, 5, 14, 22, 45]

당첨 번호를 입력해 주세요.
1,2,3,4,5,6

보너스 번호를 입력해 주세요.
7

당첨 통계

3개 일치 (5,000원) - 1개
4개 일치 (50,000원) - 0개
5개 일치 (1,500,000원) - 0개
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
6개 일치 (2,000,000,000원) - 0개
총 수익률은 62.5%입니다.

🎯 프로그래밍 요구 사항

  • Node.js 14 버전에서 실행 가능해야 한다. Node.js 14에서 정상적으로 동작하지 않을 경우 0점 처리한다.
  • 프로그램 실행의 시작점은 App.js의 play 메서드이다. 아래와 같이 프로그램을 실행시킬 수 있어야 한다.

예시

1
2
const app = new App();
app.play();
  • package.json을 변경할 수 없고 외부 라이브러리(jQuery, Lodash 등)를 사용하지 않는다. 순수 Vanilla JS로만 구현한다.
  • JavaScript 코드 컨벤션을 지키면서 프로그래밍 한다
  • 프로그램 종료 시 process.exit()를 호출하지 않는다.
  • 프로그램 구현이 완료되면 ApplicationTest의 모든 테스트가 성공해야 한다. 테스트가 실패할 경우 0점 처리한다.
  • 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 이름을 수정하거나 이동하지 않는다.
  • indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
    • 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
    • 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.
  • 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
  • Jest를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.

추가된 요구 사항

  • 함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다.
    • 함수(또는 메서드)가 한 가지 일만 잘 하도록 구현한다.
  • else를 지양한다.
    • 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
    • 때로는 if/else, switch문을 사용하는 것이 더 깔끔해 보일 수 있다. 어느 경우에 쓰는 것이 적절할지 스스로 고민해 본다.
  • 도메인 로직에 단위 테스트를 구현해야 한다. 단, UI(Console.readLine, Console.print) 로직에 대한 단위 테스트는 제외한다.
    • 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.
    • 단위 테스트 작성이 익숙하지 않다면 tests/LottoTest.js를 참고하여 학습한 후 테스트를 구현한다.

라이브러리

  • MissionUtils 라이브러리에서 제공하는 Random 및 Console API를 사용하여 구현해야 한다.
    • Random 값 추출은 MissionUtils 라이브러리의 Random.pickUniqueNumbersInRange()를 활용한다.
    • 사용자의 값을 입력 받고 출력하기 위해서는 MissionUtils 라이브러리에서 제공하는 Console.readLine, Console.print를 활용한다.

사용 예시

1
const numbers = MissionUtils.Random.pickUniqueNumbersInRange(1, 45, 6);

Lotto 클래스

  • 제공된 Lotto 클래스를 활용해 구현해야 한다.
  • numbers의 # prefix를 변경할 수 없다.
  • Lotto에 필드를 추가할 수 없다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Lotto {
#numbers;

constructor(numbers) {
this.validate(numbers);
this.#numbers = numbers;
}

validate(numbers) {
if (numbers.length !== 6) {
throw new Error();
}
}

// TODO: 추가 기능 구현
}

회고

🐥 당첨 번호 및 로또 번호 예외 처리

  • 당첨번호 에러 처리할 때 중복검사 뿐만 아니라 범위도 확인해줘야 한다. 게다가 보너스 번호는 당첨번호와 중복 검사도 추가로 해줘야 한다.

🦉 typeof NaN === ‘number’

  • “4r”이라는 문자열을 숫자로 형변환 해주면 NaN 값이 나오게된다. 에러처리를 할 때 type이 ‘number’인지를 확인해주도록 하였는데, NaN은 number 타입을 가지고 있으므로 이에 대한 처리를 추가해줘야했다.

🦆 하드코딩 vs 소프트코딩

  • score 객체에 3등 4등 5등 의 키 값은 숫자로만 주었는데, bonus 등수는 문자열로 주어서 출력시 5등 4등 3등 2등 1등 Bonus등수가 출력되어서 이를 해결하기 위해 score의 key 값을 문자열로 변경하였다.

  • lotto 배열의 요소도 [1, 2, 3, 4, 5, 6] 이런 형태의 배열인데, print 결과물이 “[1, 2, 3, 4, 5, 6]” 이렇게 나와야 하므로 이에 대한 처리를 해주었다.

  1. 백틱만 사용
1
this.Lotto.forEach((lotto) => MissionUtils.Console.print(`${lotto}`));

Error1

  1. 문자열에 [ ] 삽입
1
this.Lotto.forEach((lotto) => MissionUtils.Console.print(`[${lotto}]`));

Error2

  1. 문자열 배열처럼 처리
1
2
3
this.Lotto.forEach((lotto) =>
MissionUtils.Console.print(`[${lotto.join(", ")}]`)
);

Error3

🐒 숫자를 천 단위 콤마찍은 문자열로 변환하기

  • Number.prototype.toLocaleString() 메서드를 사용하면 천단위에 ,를 찍은 문자열로 변환한다.

✏️ 피드백 (22.11.17 추가)

비즈니스 로직과 UI로직 구분

  • 한 클래스에서 담당하지 않고 구분하여야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
class Lotto {
#numbers

// 로또 숫자가 포함되어 있는지 확인하는 비즈니스 로직
contains(numbers) {
...
}

// UI 로직
print() {
...
}
}

객체의 상태 접근 제한

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 내 코드
class App {
constructor() {
this.LottoCount = null;
this.Lotto = [];
this.winNum = [];
this.bonusNum = null;
this.score = { "3개": 0, "4개": 0, "5개": 0, bonus개: 0, "6개": 0 };
this.prize = {
"3개": 5000,
"4개": 50000,
"5개": 1500000,
bonus개: 30000000,
"6개": 2000000000,
};
this.totalMoney = null;
}
}
  • 위 와 같은 경우 객체의 데이터를 외부에서 접근 가능하므로 이는 디버깅과 유지보수를 어렵게하므로 접근 제한자를 설정하고 선택된 메서드만으로 데이터를 참조하거나 변경할 수 있도록 개선한다.

객체는 객체답게 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Lotto {
#numbers

constructor(numbers) {
this.#numbers = numbers
}

getNumbers() {
return this.#numbers
}
}

class LottoGame {
play() {
const lotto = new Lotto(...)

// 숫자가 포함되어 있는지 확인한다.
lotto.getNumbers().contains(number)
// 당첨 번호와 몇 개가 일치하는지 확인한다.
lotto.getNumbers().stream()...
}
}
  • 로또 클래스는 로또 숫자를 가져오는 역할만 한다.
  • 로또게임 클래스는 play() 메서드를 호출 하였을 경우 이와 관련된 로직으로 구성한다.
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
class Lotto {
#numbers

constructor(numbers) {
this.#numbers = numbers
}

contains(number) {
// 숫자가 포함되어 있는지 확인한다.
return ...
}

matchCount(other) {
// 당첨 번호와 몇 개가 일치하는지 확인한다.
return ...
}
}

class LottoGame {
play() {
const lotto = new Lotto(...)

lotto.contains(number)
lotto.matchCount(...)
}
}
  • getter를 사용하여 값을 꺼내 사용하는 대신 다른 객체에 메시지를 건네주자. 객체가 스스로 일하도록 하는 객체 지향 프로그래밍 기법을 사용하자.
  • 출력을 위한 값, 순수 값 프로퍼티를 가져오기 위해서라면 getter를 허용한다.

필드 수를 줄이도록 하자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 리팩터링 전
class LottoResult {
#result = new Map()
#profitRate
#totalPrize
}

// 리팩터링 후
class LottoResult {
#result = new Map()

calculateProfitRate() { ... }

calculateTotalPrize() { ... }
}
  • 위 객체의 profitRate와 totalPrize는 등수 별 당첨 내역(result)만 있어도 모두 구할 수 있는 값이다. 따라서 위 객체는 다음과 같이 하나의 필드만으로 구현할 수 있다.

🏓 소감

2주차때 클래스구조와 테스트에 대해서 조금은 익숙해져서 3주차 미션을 받았을 때 구조가 비슷하여 조금 마음이 놓였다.

  • jest expect 함수와 matcher 함수를 찾아보면서 단위 테스트 기능에 대해 공부해볼 수 있는 기회여서 좋았다.
  • 로또 클래스와 비즈니스 로직 클래스를 구분하여 테스트를 진행하고 관심사를 분리하면서 개발을 하니 유지보수가 편리하다는 것을 느낄 수 있었다.

댓글 공유

🎧 PropTypes

카테고리 React

🌈 PropTypes

리액트 컴포넌트에 전달되는 Prop(속성)의 Type(타입)을 검사하는 방법에 대해 알아보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
EmotionCard.propTypes = {
// 전달 속성 객체, 속성 이름, 컴포넌트 이름
emotion(props, propName, componentName) {
// 체크할 유형
const checkType = "string";
// 전달 속성 유형
const propType = typeof props[propName];

// 전달 속성 검사 (문자 값인지 확인)
if (propType !== checkType) {
// 문자 값이 아닌 경우 오류 발생
throw new Error(
`${componentName} 컴포넌트에 전달 된 속성 ${propName}의 데이터 유형은 ${checkType}이 요구되나, 실제 전달된 속성 유형은 ${propType}이니 확인 바랍니다.`
);
}
},
};

위와 같이 직접 custom propTypes를 통해 prop를 검사할 수 있지만 리액트 팀에서 제공하는 propTypes 패키지를 사용하면 편리하고 안정적으로 prop 검사를 할 수 있다.

패키지 사용

설치

1
npm i -D prop-types

사용 예시

1
2
3
4
5
6
7
8
9
10
import PropTypes from "prop-types";

// ...

Worker.propTypes = {
name: PropTypes.string.isRequired,
career: PropTypes.number,
onCareerUp: PropTypes.func,
isLeave: PropTypes.bool,
};
  • isRequired 는 필수입력을 요구하는 속성을 표시한다.

propTypes 검사 항목

propType_categories

PropTypes﹕objectOf vs. shape

1
2
3
4
5
6
7
8
9
10
import { objectOf, number } from "prop-types";

const geoProps = {
latitude: 37.331706,
longitude: -122.030783,
};

ReactComponent.propTypes = {
geoProps: objectOf(number),
};
  • propTypes.objectOf객체의 속성 값이 모두 동일한 타입을 설명할 경우 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
import { shape, arrayOf, string } from "prop-types";

const personProp = {
name: "야무",
job: ["강사", "디자이너", "개발자"],
};

ReactComponent.propTypes = {
personProp: shape({
name: string,
job: arrayOf(string),
}),
};
  • 내가 객체 prop 타입 설정하고 싶다하면 shape를 사용한다.
  • propTypes.shape 는 객체의 각 속성별 타입을 설명할 때 사용한다.
  • 객체의 name 속성은 string, job 속성은 배열을 사용하였다.
  • job의 배열에는 string으로만 구성된 배열이므로 arrayOf를 사용하였다.

객체의 속성이 정확히 동일하게 일치해야한다면 propTypes.exact()를 사용한다.

❗️ Null 타입 체크 주의

propTypes는 null 타입 체크를 할 수 없어 oneOf를 사용한다.

1
2
3
4
5
6
7
8
9
10
11
import { oneOf, oneOfType, shape, string } from "prop-types";

SignInedInfo.propTypes = {
authUser: oneOfType([
oneOf([null]), // ← 이렇게 작성합니다.
shape({
displayName: string.isRequired,
photoURL: string.isRequired,
}),
]).isRequired,
};
  • PropTypes.oneOf([’grow’, ‘learn’, ‘connect’]) 이 중 하나만 파라미터로 들어올 수 있다.

🔥 defaultProps와 같이 사용하기

함수에 기본값을 주듯이 props에 기본값을 주는 것을 리액트에서는 지양한다. 그러므로 다음과 같이 기본값을 줘야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react';
import { number, bool}

const Worker = ({ name, career, onCareerUp, isLeave }) => (
// ...
);

// Worker 컴포넌트 전달 속성 기본 값 설정
Worker.defaultProps = {
career: 0,
isLeave: true,
}

Worker.propTypes = {
career:number,
isLeave: bool
}

export { Worker };
  • boolean 대신 bool이라고 사용합니다.

댓글 공유

애플리케이션의 비동기 처리는 빈번하게 발생하므로 비동기 요청의 응답을 기다리는 동안 사용자에게 로딩을 표시해줘야한다. 재사용이 가능한 컴포넌트로 로딩 컴포넌트를 만들어보자.

✏️ 접근성 고려

접근성을 고려하였을 때, 스크린 리더가 로딩중이 시작할 때와 로딩이 종료되었을 때를 읽을 수 있도록 하기 위해서는 public 폴더에 index.html 파일에 다음과 같이 기재가 되어있어야한다.

1
2
3
4
5
6
// public/index.html
<body>
<!-- 로딩 스피너 접근성을 위한 DOM 요소를 추가하세요. -->
<div id="loading-start" aria-live="assertive"></div>
<div id="loading-end" aria-live="assertive"></div>
</body>
  • aria-live="assertive"속성을 주어 다른 것보다 우선적으로 스크린 리더가 읽도록 설정해준다.

예제

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
// Spinner.js
const loadingElements = {
start: document.getElementById("loading-start"),
end: document.getElementById("loading-end"),
};

export class Spinner extends React.Component {
static defaultProps = {
type: "connect", // 'grow', 'learn', 'connect'*
message: "로딩 중...",
showMessage: true,
timeToDisappear: {
start: 1500,
end: 2500,
},
};

render() {
const { type, message, showMessage } = this.props;
const spinnerImagePath = getAsset(`spinner/spinner-${type}.gif`);

return (
<figure className={styles.container}>
<img className={styles.image} src={spinnerImagePath} alt="" />
{showMessage ? (
<figcaption>{message}</figcaption>
) : (
<A11yHidden as="figcaption">{message}</A11yHidden>
)}
</figure>
);
}

componentDidMount() {
const { start } = loadingElements;
start.setAttribute("role", "alert");
start.insertAdjacentHTML(
"beforeend",
`<span class="a11yHidden">${this.props.message}</span>`
);
}

componentWillUnmount() {
const { start, end } = loadingElements;
const { timeToDisappear } = this.props;

setTimeout(() => {
start.removeAttribute("role");
start.innerHTML = "";
end.insertAdjacentHTML(
"afterbegin",
`<span class="a11yHidden">로딩이 종료되었습니다.</span>`
);
}, timeToDisappear.start);
setTimeout(() => {
end.innerHTML = "";
}, timeToDisappear.end);
}
}
  1. loading 요소를 반복적으로 사용할 것이기 때문에 최상단에 객체의 프로퍼티로 등록시켜주었다.
  2. Spinner 컴포넌트의 기본 props값을 설정해주었다.
    • 이는 컴포넌트를 만든 사람만 알 수 있기때문에 문서화를 하거나 TypeScript를 사용하여 개발자 경험(DX)를 높일 수 있다.
  3. 로딩중이라는 메시지를 보여주는 경우와 그렇지 않는 경우를 나누었다. 보여주지 않는 경우에는 접근성 컴포넌트로 생성하여 스크린 리더에는 읽히도록 설정해주었다.
  4. 컴포넌트가 mounted 될 때, role="alert"속성을 주어 스크린 리더가 읽고 있는 것을 중지하고 로딩중을 읽도록 설정하였다.
  5. StrictMode 에서는 mounted - unmounted - mounted 되는 특징때문에 2번 작동할 수 있으므로 성능을 고려하여 clean Up을 해줘야한다.

댓글 공유

Portal(포털)

카테고리 React

📌 Portal이란?

포털을 사용하면 애플리케이션 영역을 벗어나 특정 위치에 컴포넌트를 렌더링할 수 있다. 즉, 지금껏 root 컨테이너에만 렌더링을 해왔다면 포털을 사용하여 root 컨테이너 외부에다가도 컴포넌트를 렌더링할 수 있게된다.

포털을 통해 렌더링된 컴포넌트는 DOM 트리 위치와 상관없이 React 컴포넌트 트리에 포함되기 때문이다.

🔨 사용방법

1
ReactDOM.createPortal(child, container);
  • child는 렌더링할 수 있는 자식
  • container는 DOM 요소이다.
1
2
3
4
5
6
render() {
return ReactDOM.createPortal(
this.props.children,
domNode
)
}
  • 위 경우 React는 새로운 div를 생성하지 않고 domNode 안에 자식을 렌더링한다.
  • domNode는 DOM 내부의 어디에 있던지 간에 상관없다.

시각적으로 자식을 튀어나오도록 보여야하는 다이얼로그, 호버카드, 툴팁에 사용된다. 이 때, 키보드 포커스 관리와 접근성을 고려해줘야한다.

🌈 다이얼로그 예시

다이얼로그 특징

  1. 다이얼로그가 띄워진 상태면 그 아래 위치한 내용은 비활성화 상태여야한다.
  2. 다이얼로그 바깥으로 초점이동되면 안된다.
  3. dialog 역할(role)을 부여해야한다.
  4. 모달 기능일 경우 aria-modal=true 이여야한다.
  5. 다이얼로그 제목은 aria-label, aria-labelledby로 설정한다.

1. 부모 컴포넌트

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
export class DemoDialog extends React.Component {
#opennerRef = React.createRef(null);

state = {
show: false,
};

handleShowDialog = () => {
this.setState({ show: true });
};

handleHideDialog = () => {
this.setState({ show: false });
};

render() {
return (
<div className={styles.box}>
<button
ref={this.#opennerRef}
type="button"
className={styles.openDialogButton}
aria-haspopup="dialog"
aria-label="모달 다이얼로그 열기"
title="모달 다이얼로그 열기"
onClick={this.handleShowDialog}
>
모달열기
</button>
{this.state.show && (
<Dialog
modal
onClose={this.handleHideDialog}
openner={this.#opennerRef.current}
>
<Dialog.Header>
<h3>불금 다이얼로그</h3>
</Dialog.Header>
<Dialog.Body>
<ul>...</ul>
</Dialog.Body>
</Dialog>
)}
</div>
);
}
}
  • 부모 컴포넌트에서는 버튼 클릭이벤트로 모달을 조건부 렌더링해주고 있다.

2. 모달 컴포넌트(자식)

root 요소가 아닌 곳에 자식 컴포넌트를 그려주기 위해서 public 폴더의 index.html에 다음과 같이 div 요소를 추가해줘야한다.

1
2
3
4
5
6
7
// public/index.html
<body>
<div id="root"></div>

<!-- 다이얼로그를 렌더링 할 컨테이너 요소 -->
<div id="dialogZone"></div>
</boyd>
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
// Dialog.jsx
const { documentElement: htmlElement } = document;
const reactDomContainer = document.getElementById("root");

export class Dialog extends React.Component {
#containerRef = React.createRef(null);

handleClose = () => {
this.props.onClose?.();
this.props.openner.focus();
};

render() {
const { modal = false } = this.props;

return createPortal(
<>
<article
ref={this.#containerRef}
tabIndex={-1}
role="dialog"
aria-modal={modal}
className={styles.container}
>
{this.props.children}
<Dialog.Footer onClose={this.handleClose} />
</article>
{modal && <div className={styles.dim} onClick={this.handleClose} />}
</>,
document.getElementById("dialogZone")
);
}
}
  • 우선 해당 모달 컴포넌트를 그릴 domNode를 htmlElement로 지정해주었다.
  • 컨테이너 DOM 요소 가져오기 위해서 ref를 생성해주었다.
  • createPortal()에 렌더링할 자식 컴포넌트와 자식 컴포넌트를 렌더링할 컨테이너를 전달하였다.
  • 모달이 켜져있을 때, Dialog.footer 자식 요소에게 onClose를 props로 전달하였다.

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
export class Dialog extends React.Component {
...

#tabbableElements = [];

#bindEscKeyEvents() {
const handler = (e) => {
if (e.key.toLowerCase().includes("escape")) {
console.log("pressed esc key");
this.handleClose();
}
};

document.addEventListener("keyup", handler);

// cleanup function
return () => document.removeEventListener("keyup", handler);
}

#unbindEscKeyEvents = null;

componentDidMount() {
this.#containerRef.current.focus();
this.#tabbableElements = getTabbableElements(this.#containerRef.current);
this.settingKeyboardTrap();

htmlElement.style.overflowY = "hidden";
reactDomContainer.setAttribute("aria-hidden", true);

this.#unbindEscKeyEvents = this.#bindEscKeyEvents();
}

componentWillUnmount() {
htmlElement.style.overflowY = "visible";
reactDomContainer.setAttribute("aria-hidden", false);

this.#unbindEscKeyEvents?.();
}

settingKeyboardTrap() {
const tabbles = this.#tabbableElements;
const firstElement = tabbles[0];
const lastElement = tabbles[tabbles.length - 1];

firstElement.addEventListener("keydown", (e) => {
if (e.shiftKey && e.key.toLowerCase().includes("tab")) {
e.preventDefault();
lastElement.focus();
}
});

lastElement.addEventListener("keydown", (e) => {
if (!e.shiftKey && e.key.toLowerCase().includes("tab")) {
e.preventDefault();
firstElement.focus();
}
});
}
}
  • key 이벤트를 등록해주고 제거해줄 때, 클로저를 사용하여 이벤트 핸들러를 동일한 참조값으로 일치시켜주면 편하다.
    • keydown 이벤트를 사용해야지만 e.preventDefault()기본동작을 막을 수 있다.
  • 모달 컴포넌트가 생성되었을 때, 해당 컨테이너(article)에 초점을 가게하기 위해 ref를 전달해준 것이다.
  • htmlElement.style.overflowY = "hidden"로 모달 컴포넌트가 띄워져있을 때, 다른 요소는 스크롤이 비활성화시켜주었다.
  • 모달이 켜져있으면 root 컨테이너는 aria-hidden을 스크린 리더기에서도 모달만 읽히도록 해줘야한다.
  • 컴포넌트가 소멸되기 직전에 등록했던 key 이벤트를 제거해줘야하는 것을 잊지 말자. (고려하지 않는다면 성능에 문제가 생길 것이다.)

4. 모달 slot 구분

어떤 컴포넌트들은 어떤 자식 요소가 들어올지 예상할 수 없는 경우가 있다. 이럴 경우 children prop을 사용하여 자식 요소를 출력에 그대로 전달하는 것이 좋다.

합성(composition)을 사용하여 컴포넌트 간에 코드를 재사용하도록 하자.

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
Dialog.Header = function DialogHeader({ children }) {
return <header className={styles.header}>{children}</header>;
};

Dialog.Header.defaultProps = {
children: <h2>React 포털로 연 다이얼로그(with 모달)</h2>,
};

Dialog.Body = function DialogBody({ children }) {
return <div className={styles.body}>{children}</div>;
};

Dialog.Footer = function DialogFooter({ children, onClose }) {
return (
<footer className={styles.footer}>
<button
type="button"
className={styles.closeButton}
aria-label="모달 다이얼로그 닫기"
title="모달 다이얼로그 닫기"
onClick={onClose}
>
모달닫기
</button>
{children}
</footer>
);
};

댓글 공유

DOM 컨트롤(ref)

카테고리 React

🕹 리액트 DOM 컨트롤

리액트 앱은 대부분 리액트에 의해 컨트롤되지만 예외인 상황이 있다. 리액트는 virtualDOM을 가지고 동작하기 때문에 RealDOM 요소에 접근하거나 조작해야할 경우 이는 리액트가 할 수 없는 일이다.

이러한 일을 리액트 사이드 이펙트라고 부른다. 흔히들 사이드 이펙트를 부작용이라고 오인하는 경우가 있지만 여기서는 순수하지 않거나 역할에 맞는 일을 하지 않는 경우를 말한다.

사이드 이펙트를 처리하기 위해서는 다음과 같은 React API를 사용하여야한다.

  • ref(참조 설정)
    • 값이 변경되어도 리액트가 재렌더링하지 않는다.
  • callback ref(ref 속성에 연결된 함수)
  • React.createRef(참조 객체 생성)
  • React.forwardRef(참조 객체 전달)
    • 범용적으로 누구나 사용할 수 있는 컴포넌트를 만들기 위해서 꼭 필요하다.
    • 외부 컴포넌트가 내부 컴포넌트를 전달받아서 컴포넌트를 내보낸다.
    • domRef를 통해서 전달할 수도 있다.

💭 예시

컴포넌트 생성 시점에 이벤트를 구독한 경우, 컴포넌트 제거 시점에 구독한 이벤트를 취소해야한다.

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
export class TiltCard extends React.Component {
// DOM 요소 참조를 목적으로 Ref를 생성합니다.
tiltRef = React.createRef(null); // { current:null }

// DOM 요소 참조를 위한 목적의 클래스 인스턴스 멤버
#cardDomElement = null;

// 최초 마운트 시점 이후 처리할 로직을 작성합니다.
componentDidMount() {
this.#cardDomElement = this.tiltRef.current;
VanillaTilt.init(this.#cardDomElement);

// 이벤트 구독
this.#cardDomElement.addEventListener(
"tiltChange",
this.handleChangeTilt.bind(this)
);
}

// 마운트 해제 직전에 처리할 로직을 작성합니다.
componentWillUnmount() {
this.#cardDomElement.vanillaTilt.destroy();

// 이벤트 구독 해지
this.#cardDomElement.removeEventListener(
"tiltChange",
this.handleChangeTilt.bind(this)
);
}

render() {
const { children } = this.props;

return (
// 생성된 Ref를 참조하도록 설정합니다.
<div ref={this.tiltRef} className="tiltCard">
{children}
</div>
);
}
}
  • tiltRef에 DOM 요소를 저장하기 위해 ref를 생성하였다.
  • tiltRef DOM 요소 얻기 위해 “current” 프로퍼티에 접근하였다.
  • tiltRef DOM 요소에 컴포넌트 생성주기에 맞게 이벤트를 등록하고 제거해주었고 ref를 참조하도록 설정해주었다.

댓글 공유

🥁 CORS

카테고리 CS

📌 CORS란?

교차 출처 리소스 공유(Cross-Origin Resource Sharing)는 추가 HTTP Header를 사용하여 한 출처에서 실행 중인 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에게 알려준다.

웹 애플리케이션은 리소스가 자신의 출처(도메인, 프로토콜, 포트)와 다를 때, 교차 출처 HTTP 요청을 실행한다.

ex) https://domain-a.com의 프론트 엔드 JavaScript 코드가 XMLHttpRequest를 사용하여 https://domain-b.com/data.json을 요청하는 경우 보안 상의 이유로 브라우저는 HTTP 요청을 제한한다.

  • CORS 체제는 브라우저와 서버 간의 안전한 교차 출처 요청 및 데이터 전송을 지원한다.
  • CORS 표준에 맞춘다는 것은 서버에서도 새로운 요청과 응답 Header를 처리해야한다.

🤿 DeepDive

CORS 표준은 웹 브라우저에서 허용된 출처를 서버에서 새로운 HTTP Header에 추가함으로써 동작한다.

📮 Simple Request

HTTP 요청 메서드가 GET, POST 일 때 사용한다.

  1. 다른 출처끼리 요청을 보낼 때, 요청에 Origin이라는 Header를 추가한다.

    1
    https://loco9939.com:5000
    • Origin은 요청하는 쪽의 Scheme, 도메인, 포트가 담겨있다.
    • scheme : https
    • 도메인 : loco9939.com
    • 포트 : :5000
  2. 요청 받은 서버는 응답 Header에 지정된 ACAO(Access Control Allow Origin) 정보를 실어서 보낸다.

  3. 브라우저가 ACAO 정보가 담긴 응답과 요청의 Origin을 비교하여 동일하면 허락한다.

✈️ Preflight

브라우저가 사전요청을 먼저 보낸 후 서버의 응답을 보고 안전한지 확인한 후 본 요청을 보내는 방식이다. 본 요청은 Simple Request 방식과 동일하다.

🌈 CORS 오류 해결 방법

🔨 서버 측

  • ACAO(Access Control Allow Origin) 설정하기
    • 구체적인 출처 명시하기
    • Credentials: include 옵션 사용한 경우 * 사용한 경우 CORS 에러발생
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var http = require("http");

const PORT = process.env.PORT || 3000;

var httpServer = http.createServer(function (request, response) {
// Setting up Headers
response.setHeader("Access-Control-Allow-origin", "*"); // 모든 출처(orogin)을 허용
response.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, OPTIONS, PUT, PATCH, DELETE"
); // 모든 HTTP 메서드 허용
response.setHeader("Access-Control-Allow-Credentials", "true"); // 클라이언트와 서버 간에 쿠키 주고받기 허용

// ...

response.writeHead(200, { "Content-Type": "text/plain" });
response.end("ok");
});

httpServer.listen(PORT, () => {
console.log("Server is running at port 3000...");
});

Access-Control-Allow-origin 헤더 값으로 * 을 사용하면 모든 Origin에서 오는 요청을 허용한다는 의미이므로 당장은 편할 수 있겠지만, 바꿔서 생각하면 정체도 모르는 이상한 출처에서 오는 요청까지 모두 허용하기 때문에 보안은 더 허술해진다. 그러니 가급적이면 귀찮더라도 다음과 같이 출처를 직접 명시해주도록 하자.

👑 클라이언트 측

  • Proxy 설정
    • 브라우저 요청을 Proxy에서 설정한 주소로 우회하여 전송하는 방법
    • 라이브러리 사용한다
    • 개발단계에서만 사용하고 Production 환경에서는 Proxy 처리가 되지 않는다.

댓글 공유

⛳️ MVC, MVP, MVVM 패턴

카테고리 CS

📌 디자인 패턴이란?

디자인 패턴은 애플리케이션내에서 각자 역할에 맞는 코드끼리 분리하여 유지보수성을 높이고 탄탄한 구조를 가진 애플리케이션을 설계할 수 있다.

✏️ 디자인 패턴 장점

  1. 검증된 해결책

디자인 패턴은 소프트웨어 개발의 문제를 해결하는 확실한 접근 방식을 제공한다.

  1. 쉬운 재사용

일반적으로 필요에 따라 조정가능하며 즉시 사용가능한 솔루션을 반영한다.

다만, 디자인 패턴이 모든 문제의 해결책은 아니다. 패턴의 역할은 솔루션 체계를 제공하고 이를 지원하는 역할을 한다.

이외의 사소한 문제를 예방할 수 있고 반복을 피하여 파일 크기를 줄일 수 있으며, 개발자간의 소통을 원할하게 하는 장점이 있다.

🛤 MVC 패턴

Model + View + Controller를 합친 용어이다.

mvc

  • Model : 데이터와 데이터 변경 함수 관리하는 부분
  • View : 사용자에게 보여지는 UI
  • Controller : 사용자의 입력을 받고 처리하는 부분

동작

  1. 사용자 액션이 들어오면 컨트롤러가 액션 확인하고 모델을 업데이트한다.
  2. 컨트롤러는 모델을 나타낼 뷰를 선택한다.
  3. 뷰는 모델을 이용하여 화면에 나타낸다.
1
2
3
4
참고 - MVC에서 View가 업데이트 되는 방법
- 뷰가 모델을 이용하여 직접 업데이트
- 모델이 뷰에게 알림을 주어 업데이트
- 뷰가 Polling으로 모델의 변경을 주기적으로 감지하여 업데이트

polling이란?

하나의 장치가 충돌 회피 또는 동기화를 목적으로 다른 장치의 상태를 주기적으로 검사하여 일정 조건을 만족할 때, 송수신 자료를 처리하는 방식


특징

  • 컨트롤러는 여러 개의 뷰를 선택할 수 있는 1:n 구조
  • 보편적이며 단순하다.
  • 뷰와 모델 사이의 의존성이 높다. (bad~👎)

🏸 MVP 패턴

Model + View + Presenter를 합친 용어이다.

mvp

  • Model : 데이터와 데이터 변경 함수 관리하는 부분
  • View : 사용자에게 보여지는 UI
  • Presenter : 뷰에서 요청한 정보로 모델을 가공하여 뷰에게 전달해주는 부분(View, Model을 붙여주는 접착제역할)

동작

  1. 사용자 액션은 뷰를 통해 들어온다.
  2. 뷰는 프레젠터에게 데이터를 요청한다.
  3. 프레젠터는 모델에게 데이터 요청한다.
  4. 모델은 프레젠터에게 요청받은 데이터를 응답한다.
  5. 프레젠터는 뷰에게 데이터를 응답한다.
  6. 뷰가 응답받은 데이터를 화면에 나타낸다.

특징

  • 프레젠터는 뷰와 모델의 인스턴스를 가지고 있어 둘을 연결하는 접착제 역할
  • 프레젠터와 뷰는 1:1
  • 뷰와 모델간의 의존성이 없다. (Good~👍)
  • 하지만 뷰와 프레젠터의 의존성 높다.

👑 MVVM 패턴

Model + View + View Model을 합친 용어이다.

mvvm

  • Model : 데이터와 데이터 변경 함수 관리하는 부분
  • View : 사용자에게 보여지는 UI
  • View Model : 뷰를 표현하기 위해 만든 뷰를 위한 모델

동작

  1. 사용자 액션이 뷰를 통해 들어온다.
  2. 뷰에 액션이 들어오면 Command 패턴으로 뷰 모델에 액션을 전달한다.
  3. 뷰 모델은 모델에게 데이터를 요청한다.
  4. 모델은 뷰 모델에게 요청받은 데이터를 응답한다.
  5. 뷰 모델은 응답받은 데이터를 가공하여 저장한다.
  6. 뷰는 뷰 모델과 데이터 바인딩하여 화면에 나타낸다.

특징

  • Command 패턴과 데이터 바인딩 두가지 패턴을 사용(뷰와 뷰 모델 사이의 의존성 제거)
  • 뷰 모델과 뷰는 1:n
  • 뷰와 모델사이의 의존성도 없으므로 각 부분은 독립적으로 모듈화하여 개발할 수 있다.
  • 뷰 모델 설계가 어렵다.

Command패턴이란?

요청을 객체 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 사용할 수 있도록 메서드명, 매개변수 등 요청에 필요한 정보를 저장,로깅, 취소하는 패턴이다.


참고

[디자인패턴]MVC,MVP,MVVM 패턴 비교

댓글 공유

🎡 Flux 패턴

카테고리 CS

📌 Flux 패턴

페이스북이 전통적인 MVC 디자인 패턴이 복잡한 애플리케이션에서 적합하지 않다고 판단하여 새로운 디자인 패턴을 소개한 것이 바로 단방향 데이터 흐름을 갖는 Flux 패턴이다.

🔥 문제점

Complicated MVC

애플리케이션 규모가 커짐에 따라 양방향 데이터 흐름을 그림처럼 복잡한 구조를 가지게 된다.

사용자와 상호작용하는 여러 View가 연결된 여러 Model을 업데이트하고 Model 또한 연결된 View를 업데이트하는 매우 복잡한 상황이 발생하기에 데이터 흐름을 예측하기란 매우 어려워진다.

사실 위 그림은 전통적인 MVC 패턴이고 Apple의 MVC 패턴은 사진처럼 Model과 View의 양방향 데이터 흐름이 발생하지 않는다. 문제는 양방향 데이터 흐름이다.

AppleMVC

🦖 Flux 아키텍처

flux

Flux 패턴은 앱의 단방향 데이터 흐름을 촉진하는 시스템 아키텍처를 말한다.

Action

사용자의 요청을 정의한 객체를 말한다.

  • 사용자 요청 타입(Type)과 데이터(Payload)를 가진다.
  • 액션 크리에이터(Action Creator) 함수를 사용하여 디스패처로 전달한다.

Dispatcher

디스패처는 중앙 데이터 흐름 관리한다. 스토어에 등록된 액션 타입마다 콜백되는 함수가 존재한다.

  • 사용자 액션으로부터 받은 액션 타입에 맞는 콜백함수를 실행한다.
  • 스토어 데이터 변경은 디스패처를 통해서만 가능하다.

Store

스토어는 상태(Model)를 관리하는 저장소이며 상태를 변경할 수 있는 콜백을 가진다. 각 액션을 처리하는 콜백함수는 디스패처에 등록된다.

  • 상태와 상태를 변경하는 함수를 가진다.
  • 디스패처에 등록된 콜백함수가 실행되면 스토어의 상태가 변경되며 View에 데이터가 변경되었음을 알린다.(옵저버 패턴 활용)

View

뷰는 사용자와 상호작용하는 컴포넌트이다. 스토어에서 상태가 변경되었다는 알림이 오면 뷰는 리렌더링이 발생한다.

  • 사용자는 View를 조작할 수 있고 상태 변경을 요청하는 액션을 생성할 수 있다.
  • 생성된 액션은 디스패처에게 전달되고 디스패처는 해당 액션에 연결된 콜백을 실행하여 스토어의 상태를 변경한다.
  • 상태가 변경되었음을 뷰에게 알리고 뷰는 리렌더링이 발생한다.

댓글 공유

loco9939

author.bio


author.job