크래프톤 정글 TIL : 1006

lazyArtisan·2024년 10월 6일
0

정글 TIL

목록 보기
98/147

🌐 게시판 과제


Modifiers cannot appear here

이 함수 다른 js 파일에서 쓰려고 export 붙였더니 오류 남.

이유 : App.js에 있는 상태 변수를 갱신하는 함수라서 그렇다고 함

에서 playerTouchingDoor이 true면
threadButtonPressed() 함수가 팝업창을 띄우게 하고 싶은데,

그러려면 app.js에 있는 showPopup()을 호출해야되잖아? 이거 어떻게 해야되는거임 export도 안되면

Phaser와 React를 함께 사용하는 경우, React의 컴포넌트 상태나 함수를 Phaser 코드에서 호출하는 방법은 모듈 시스템만으로는 해결되지 않습니다. Phaser는 React 컴포넌트 트리와 별도의 동작을 하기 때문에 직접적으로 React의 함수를 호출할 수 없으므로, 상태 관리 시스템(예: Redux, Context API)나 콜백 함수 전달 방식을 통해 이 문제를 해결할 수 있습니다.

Phaser 코드에서 React의 상태를 조작하거나 React 함수를 호출하려면 React 컴포넌트에서 Phaser로 함수를 전달해주거나, 전역 상태 관리 솔루션을 사용해 통신해야 합니다.

  1. Context API를 사용한 해결 방법
  2. App.js에서 PopupProvider로 컴포넌트 감싸기
  3. Phaser에서 showPopup 호출
  4. React와 Phaser 통합 (ReactDOM 사용)

다 뭔가 굳이 이렇게까지 해야되나 싶어서 o1-preview 사용

Solution 1: Pass a Callback Function from React to Phaser

One of the simplest and most effective methods is to pass a callback function from your React component (App.js) to your Phaser game (phaser.js). This way, when the event occurs in Phaser, you can call this callback function, which will execute showPopup() in your React component.

Solution 2: Use a Custom Event

If you prefer not to pass callbacks, another straightforward approach is to use custom events to communicate between Phaser and React.

Solution 3: Use a Global Function

As a last resort, you can define showPopup() on the global window object so that it can be accessed from anywhere.

Recommendation:

I recommend Solution 1 (passing a callback function) as it is the most straightforward and aligns well with React's component-based architecture. It avoids global variables and keeps your code organized.

Additional Notes:

Ensure Proper Scope: When passing the callback, make sure it has access to the necessary scope and state within your React component.
Cleanup: If you're adding event listeners or global functions, ensure you clean them up in your useEffect cleanup function to avoid memory leaks.
Modularization: Keep your phaser.js and App.js modular by only exposing what is necessary for communication.

  useEffect(() => {
    if (isCanvasVisible && canvasRef.current && !phaserInitialized) {
      initializePhaserGame('phaser-container', showPopup);
      // ... set phaserInitialized, etc. ...
    }
  }, [isCanvasVisible, canvasRef, phaserInitialized]);

역시나 나를 실망시키지 않았다.
그냥 함수를 인자로 주면 됐던 거임.

게시판 올릴 팝업창 구현 완료.

배경 어둡게 하는 원리

{/* 팝업창이 보이는 경우에만 렌더링 */}
      {isPopupVisible && (
        <div className="popup-overlay">
          <div className="popup">
            <h2>팝업창</h2>
            <p>이 팝업은 특정 이벤트에 의해 표시됩니다.</p>
            <button onClick={closePopup}>닫기</button>
          </div>
        </div>
      )}
/* 배경 어둡게 처리 */
.popup-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.7);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}

이거 뭐 어떤 원리로 배경이 어두워지는 건지 이해가 안되는데

이 CSS 코드에서 배경이 어두워지는 원리rgba(0, 0, 0, 0.7)position: fixed, z-index 속성을 결합하여 화면 전체에 어두운 반투명 레이어를 덮는 방식으로 동작합니다. 하나씩 원리를 설명해 드리겠습니다.

1. position: fixed화면 전체 덮기

position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
  • position: fixed: 요소를 화면에 고정합니다. 이 속성은 뷰포트(viewport)에 대해 상대적으로 위치하게 하므로, 페이지를 스크롤해도 해당 요소가 화면에 고정되어 움직이지 않습니다.
  • top: 0; left: 0;: 요소가 화면의 왼쪽 상단 모서리(좌표 0, 0)에서 시작하도록 위치를 설정합니다.
  • width: 100%; height: 100%;: 이 요소가 화면의 전체 너비와 높이를 덮도록 설정합니다. 결과적으로 이 요소는 페이지의 전체 영역을 덮는 레이어가 됩니다.

2. 배경 색상 설정: rgba(0, 0, 0, 0.7)

background-color: rgba(0, 0, 0, 0.7);
  • rgba(0, 0, 0, 0.7)반투명한 검정색입니다.
    • rgb(0, 0, 0)검정색을 나타내고,
    • 0.7은 투명도를 나타내는 알파 값입니다. 이 값은 0(완전 투명)에서 1(완전 불투명) 사이의 값을 가질 수 있으며, 0.7은 70% 불투명하고 30% 투명한 상태를 의미합니다.

따라서, 화면에 이 검정색 레이어가 덮이지만 투명도가 적용되기 때문에 배경이 어둡게 보입니다.

3. 배경 위에 팝업을 중앙에 배치

display: flex;
justify-content: center;
align-items: center;
  • display: flex: flexbox 레이아웃을 사용하여 자식 요소를 중앙에 배치할 수 있도록 합니다.
  • justify-content: center: 수평 방향으로 자식 요소를 중앙에 배치합니다.
  • align-items: center: 수직 방향으로 자식 요소를 중앙에 배치합니다.

이 설정은 팝업이 화면의 중앙에 표시되도록 해줍니다.

4. z-index로 요소의 순서 제어

z-index: 1000;
  • z-index: 요소의 쌓임 순서를 정의합니다. 값이 클수록 화면의 앞쪽에 나타납니다.
  • 여기서 z-index: 1000을 사용하여 이 레이어가 다른 콘텐츠 위에 쌓이도록 설정하고, 화면의 가장 앞쪽에 배경을 덮는 검정색 오버레이를 표시합니다.

게시판 구현

리액트는 포트 3000에 열려있고 node.js는 포트 5000에 열려있음. 그냥 이대로 실행하면 원래 알아서 연결되는게 정상이야? 내가 뭐 따로 해주는거 없어도?

React 애플리케이션이 포트 3000에서 실행되고, Node.js 서버가 포트 5000에서 실행되고 있을 때, 브라우저에서 크로스 오리진 리소스 공유(CORS) 문제가 발생할 수 있습니다. 기본적으로 포트 3000과 포트 5000은 다른 오리진으로 간주되기 때문에, 서버에서 CORS 설정을 해주어야 React에서 Node.js 서버로 데이터를 요청할 수 있습니다.

나온다. 나이스.

cors 지정할 때 3000같은 거 안 적었잖음. 이렇게 되면 신뢰 못하는 놈들도 마구 들어오는거 아님?

뭔가 이상하다 했다.

근데 포트만 한정하면 어차피 의미 없는거 아님? 브루트포스로 다 확인해보면 그만이잖아. 의미 없어서 그냥 포트 지정 안 했던거임? 현업에선 어떻게 보안 강화함?

맞습니다. 포트만 한정하면 보안에 큰 의미가 없는 이유는, 포트는 누구나 쉽게 알 수 있고, 브루트포스 등으로 추측할 수 있기 때문입니다. 단순히 포트나 도메인만으로 보안을 강화하기엔 한계가 있으며, 더 나은 보안 전략이 필요합니다.

현업에서는 CORS 설정을 포함해, 여러 보안 기법을 결합해 서버와 API의 보안을 강화합니다. 다음은 현업에서 API 보안을 강화하는 여러 방법들입니다.

1. CORS를 오리진과 함께 사용 (기본적인 접근)

CORS는 기본적으로 웹 애플리케이션이 신뢰할 수 있는 오리진(도메인, 프로토콜, 포트)에서만 데이터를 요청하도록 설정할 수 있습니다. 하지만 이는 보안의 첫 단계일 뿐이며, 다음과 같은 추가적인 방법들이 필요합니다.

2. API 인증 및 권한 부여

서버는 단순히 오리진을 신뢰하는 것 외에도, 사용자가 누구인지 확인하고, 그들이 해당 API에 접근할 권한이 있는지를 검증해야 합니다.

2.1 API 키

  • API 키는 각 사용자 또는 애플리케이션에 고유한 키를 발급하여, 서버가 요청하는 클라이언트의 신원을 확인하는 데 사용됩니다. 클라이언트는 이 키를 요청 헤더에 포함하여 서버에 요청을 보냅니다.
  • 하지만 API 키만으로는 완전한 보안이 보장되지 않으며, 암호화되지 않으면 키가 노출될 위험이 있습니다.

2.2 OAuth 2.0 / JWT (JSON Web Token)

  • 현업에서는 OAuth 2.0 또는 JWT를 사용해 사용자 인증을 하는 것이 일반적입니다. JWT는 사용자가 로그인하면 발급된 토큰을 사용해 각 요청에 대한 권한을 확인할 수 있습니다.
  • 예를 들어, JWT는 클라이언트가 서버로부터 인증을 받은 후 클라이언트 측에 저장되며, 이후 요청마다 토큰을 헤더에 포함하여 서버가 해당 사용자가 인증된 사용자임을 확인할 수 있습니다.

2.3 세션 관리

  • 세션 기반 인증도 여전히 많이 사용됩니다. 사용자가 로그인하면 서버에 세션 ID를 저장하고, 이후 요청마다 브라우저에서 해당 세션 ID를 포함하여 서버가 사용자를 식별할 수 있도록 합니다.

3. HTTPS 사용

  • HTTPS통신을 암호화하여 네트워크 상에서 데이터를 전송하는 중간에 가로채는 공격을 막습니다. API 키, JWT, 쿠키와 같은 민감한 정보는 암호화되지 않은 HTTP로 전송될 때 노출될 수 있기 때문에, HTTPS를 사용해 데이터를 암호화하여 보호해야 합니다.

4. Rate Limiting (속도 제한)

  • Rate Limiting은 클라이언트가 일정 시간 내에 API에 요청할 수 있는 횟수를 제한하는 기술입니다. 이 기술은 브루트포스 공격이나 DDoS 공격을 방지하는 데 효과적입니다. 예를 들어, 특정 IP에서 초당 요청 수를 제한하여 악성 트래픽을 차단할 수 있습니다.

5. IP 화이트리스트/블랙리스트

  • IP 주소 기반으로 특정 IP 주소나 IP 범위에서만 API에 접근할 수 있도록 설정하는 방법입니다. 이는 특정한 사용자나 서버에서만 요청을 허용하고, 다른 모든 IP 주소를 차단하는 방식으로 보안을 강화할 수 있습니다.
  • 하지만 IP 스푸핑이나 공용 IP를 사용하는 상황에서는 이를 피할 수 있으므로, 다른 보안 기법과 함께 사용해야 합니다.

6. Content Security Policy (CSP)

  • CSP는 웹 애플리케이션에서 외부로부터 로드할 수 있는 리소스의 출처를 제한하는 보안 기법입니다. 이를 통해 악성 스크립트나 해커가 사이트 간 스크립트 인젝션(XSS) 공격을 하는 것을 방지할 수 있습니다.
  • 예를 들어, 브라우저가 허용된 출처에서만 스크립트, 스타일, 이미지 등을 로드할 수 있도록 강제합니다.

7. 애플리케이션 계층 보안

  • SQL 인젝션, XSS(크로스사이트 스크립팅), CSRF(크로스사이트 요청 위조) 같은 애플리케이션 보안 위협도 철저히 대비해야 합니다.
    • SQL 인젝션: 서버가 사용자 입력을 제대로 필터링하지 않을 때, 공격자가 데이터베이스 명령을 실행할 수 있는 취약점.
    • XSS: 악성 스크립트를 웹 페이지에 삽입하여, 다른 사용자의 브라우저에서 실행되도록 하는 공격.
    • CSRF: 인증된 사용자가 의도하지 않은 요청을 서버에 보내도록 유도하는 공격.

8. 모니터링 및 로깅

  • 보안 위협을 빠르게 탐지하기 위해서는 로그모니터링을 통해 서버의 활동을 추적하는 것이 중요합니다. 공격자가 비정상적인 활동을 시도하면 이를 빠르게 탐지하고 대응할 수 있어야 합니다.

보안까지 신경쓰면 좋겠지만 지금은 일단 넘어가자.

DB 구현

관계형 db 쓰는 곳과 비관계형 db쓰는 곳의 차이

그냥 몽고db 쓰면 될듯?

이 코드는 Mongoose 라이브러리를 사용하여 MongoDB에서 게시글(Post) 데이터를 저장할 구조(스키마)를 정의하고, 그 스키마를 기반으로 모델을 생성하는 과정입니다. Mongoose는 MongoDB에서 데이터를 구조적으로 다루기 쉽게 해주는 라이브러리로, 데이터 스키마를 통해 데이터를 어떻게 저장하고, 다룰지 정의할 수 있습니다.

게시글 스키마 정의 (postSchema)

const postSchema = new mongoose.Schema({
  title: String,
  content: String,
});
  • mongoose.Schema: MongoDB에 저장될 데이터의 구조를 정의하는 객체입니다. 여기서 postSchema는 게시글을 나타내며, 게시글은 두 개의 필드(title, content)를 가집니다.
  • title: String: 게시글의 제목을 저장하는 필드로, 데이터 타입은 String입니다.
  • content: String: 게시글의 내용을 저장하는 필드로, 데이터 타입은 String입니다.

이 스키마를 통해 Mongoose는 MongoDB에 데이터를 삽입할 때 해당 구조에 맞춰 데이터를 저장하도록 강제합니다. 즉, 게시글을 저장할 때는 항상 제목과 내용이 필요하고, 각각 문자열이어야 한다는 것을 정의하는 것입니다.

모델 생성 (Post)

const Post = mongoose.model('Post', postSchema);
  • mongoose.model: 스키마를 바탕으로 MongoDB와 상호작용할 수 있는 모델을 생성하는 함수입니다. 모델은 MongoDB의 컬렉션(collection)과 연결됩니다.
  • Post: 이 모델은 MongoDB에서 'posts'라는 컬렉션과 연결됩니다. Mongoose는 자동으로 모델 이름의 복수형(Postposts)으로 컬렉션을 만듭니다. 이제 이 Post 모델을 사용하여 MongoDB에서 게시글을 조회하거나, 저장하거나, 수정하거나, 삭제할 수 있습니다.

Post 모델은 MongoDB의 posts 컬렉션에 있는 데이터를 다루기 위한 인터페이스 역할을 합니다.


그니까

형식이 스키마고
db에 저장된건 컬렉션이고
그거 두 개 묶은 건 모델이고

내가 써야될 건 모델이다

데이터 올라가기 시작

// 게시글 목록 가져오기 (GET)
app.get('/api/posts', async (req, res) => {
  try {
    const posts = await Post.find(); // MongoDB에서 모든 게시글 가져오기
    res.json(posts);
  } catch (err) {
    res.status(500).json({ error: 'Server error' });
  }
});

이거 설명 좀

이 코드는 Node.jsExpress를 사용하여 MongoDB에서 게시글 목록을 가져오는 API 엔드포인트입니다. 아래는 각 부분의 설명입니다.

// 게시글 목록 가져오기 (GET)
app.get('/api/posts', async (req, res) => {
  1. app.get('/api/posts', ...):
    • ExpressGET 요청 핸들러로, /api/posts라는 경로로 들어오는 GET 요청을 처리합니다. 예를 들어, 클라이언트가 /api/posts URL로 요청을 보내면 이 핸들러가 실행됩니다.
  2. async (req, res):
    • 비동기 함수로 작성되었습니다. MongoDB에서 데이터를 가져오는 작업은 시간이 걸리기 때문에 async를 사용하여 비동기적으로 처리하고 있습니다.
    • req: 요청(request) 객체로 클라이언트가 서버로 보낸 요청에 대한 정보를 담고 있습니다.
    • res: 응답(response) 객체로 서버가 클라이언트에게 응답을 보낼 때 사용합니다.
try {
  const posts = await Post.find(); // MongoDB에서 모든 게시글 가져오기
  res.json(posts);
} catch (err) {
  res.status(500).json({ error: 'Server error' });
}
  1. try { ... } catch (err) { ... }:

    • 데이터베이스에서 데이터를 가져오는 과정에서 오류가 발생할 수 있기 때문에 try...catch 구문을 사용하여 에러 처리를 하고 있습니다.
  2. const posts = await Post.find();:

    • Post.find(): MongoDB에서 모든 게시글을 가져오는 작업입니다.
      • Post는 MongoDB의 모델로, 보통 Mongoose라는 라이브러리를 사용해 정의된 스키마를 가리킵니다.
      • find()는 MongoDB의 쿼리 메서드로, 이 모델의 모든 문서를 검색하여 결과를 반환합니다.
    • await: 이 작업은 비동기적이기 때문에 await 키워드를 사용하여 데이터베이스에서 결과가 반환될 때까지 기다립니다.
  3. res.json(posts);:

    • 데이터베이스에서 가져온 게시글 목록을 클라이언트에게 JSON 형식으로 응답합니다. 이때 posts는 MongoDB에서 가져온 게시글 데이터의 배열입니다.
  4. catch (err):

    • 만약 데이터베이스 쿼리에서 문제가 발생하면 에러가 발생합니다. 이때 이 블록이 실행됩니다.

작동 흐름:

  1. 클라이언트(예: 웹페이지)가 /api/posts 경로로 GET 요청을 보냅니다.
  2. 서버는 MongoDB에서 모든 게시글 데이터를 찾기 위해 Post.find() 메서드를 사용합니다.
  3. 게시글 데이터를 성공적으로 찾으면, 서버는 그 데이터를 JSON 형식으로 클라이언트에 응답합니다.
  4. 만약 데이터베이스에서 에러가 발생하면, 클라이언트는 500 Internal Server Error 상태 코드와 함께 에러 메시지를 받습니다.

이 코드는 주로 REST API 서버에서 게시글 목록을 클라이언트에게 제공하는 기능을 담당하며, 클라이언트는 이를 통해 게시글 데이터를 받아올 수 있습니다.

닉네임 입력란 구현

회원가입 로그인까지 구현하는 건 굳이굳이의 영역인 것 같아서
간단하게 닉네임만 입력할 수 있게 할 것.

  {/* 로그인 여부에 따라 다른 콘텐츠 렌더링 */}
  {!isLoggedIn ? (
    <div className="login-form">
      <h2>닉네임 입력</h2>
      <input type="text" placeholder="아이디" className="login-input" ref={nicknameRef} />
      &nbsp;&nbsp;&nbsp;
      <button className="login-button" onClick={handleLogin}>
        확인
      </button>
    </div>
  ) : (
    <>
      <h2>게시판</h2>
      {loading ? (
        <p>로딩 중...</p> // 로딩 중일 때 표시
      ) : (
        <div className="post-list">
          {posts.map((post) => (
            <div key={post.id} className="post-card">
              <div className="post-header">
                <div className="post-user-info">
                  <h3 className="post-title">{post.title}</h3>
                  <p className="post-username">@username</p> {/* 사용자명 */}
                </div>
              </div>
              <p className="post-content">{post.content}</p>
            </div>
          ))}
        </div>
      )}
      <button onClick={closePopup}>닫기</button> {/* 하단 닫기 버튼 */}
    </>
  )}

조건에 따라 렌더링 다르게

  // 로그인 버튼을 눌렀을 때 로그인 상태로 전환
  const handleLogin = () => {
    const nicknameValue = nicknameRef.current.value; // input 필드의 값을 가져옴
    setNickname(nicknameValue); // 닉네임 상태 업데이트
    setIsLoggedIn(true); // 로그인 상태로 전환
    console.log('닉네임:', nicknameValue);
  };

확인 누르면 서버에 보낼 닉네임 설정하는 핸들러

서버에 글 게시할 때 닉네임도 같이 보내기

    // 닉네임을 가져옴
    const nicknameInput = document.getElementById('nickname').value; 
    const nickname = nicknameInput ? nicknameInput.trim() : 'Jungler'; // 공백 제거 후 값 확인
    // 입력 필드의 데이터를 가져옴
    const inputField = document.getElementById('player-input');
    const content = inputField ? inputField.value.trim() : ''; // 공백 제거 후 값 확인

이전에 했던 거랑 똑같이 요소로 가져오면 되겠거니 했는데
그러면 안됐음. 닉네임 입력하는 요소는 이미 입력 버튼 누르면 사라진 뒤이기 때문.
그럼 그 정보를 저장할 수 있는 건 어디에 있느냐? 라고 하면

usestate인가 props인가 뭔가 하는 이상한 얘기를 또 들이밈.
저번에는 그냥 함수를 인자로 넘겨주는 방식으로 해결했는데

이건 phaser가 초기화된 이후에 데이터가 입력되는거라 그런 식으로 얼렁뚱땅 해결도 불가능.

데이터를 어딘가에 저장(브라우저 캐시? 투명한 div의 data 필드?)하면 억지로 해결할 수는 있겠지만

일단 gpt가 권장하는 정석적인 방법 시도해보기.

뭔지 잘 이해가 안 가서 클로드한테 좀 그려달라고 함.
그림이 좀 이상하게 그려지긴 했지만 어쨌든 대충 이해했다.

props : 부모가 자식한테 값(보니까 함수도 되는듯?) 주는거

자식은 그냥 부모한테 데이터 반환할 수 있는듯.

import React, { useRef } from 'react';

function Child({ onLogin }) {
  const nicknameRef = useRef(null);  // 입력 필드를 참조하기 위한 ref

  const handleButtonClick = () => {
    const inputValue = nicknameRef.current.value;  // 입력된 값을 가져옴
    onLogin(inputValue);  // 부모에게 값을 전달함
  };

  return (
    <div>
      <h2>자식 컴포넌트</h2>
      <input type="text" ref={nicknameRef} placeholder="닉네임" />
      <button onClick={handleButtonClick}>로그인</button>
    </div>
  );
}

export default Child;

자식 컴포넌트 이렇게 만들어놓고 호출하면 함수 실행도 하면서 html 요소도 만들어지는 거인듯

아 근데 너무 복잡한거 같아서 다시 물어봄

버튼 누르는 건 phaser.js에서 이루어지기 때문에 app.js에서 인자를 넘겨주는 식으로는 안된다고 생각했음. 혹시 js도 python마냥 레퍼런스만 주는거라서 phaserinitialize 할 때 인자 넘겨줘도 나중에 업데이트 되는게 반영되는거야?

JavaScript에서도 참조(Reference)가 중요한 개념입니다. Python처럼 객체는 참조로 전달되기 때문에, 객체나 배열 같은 참조형 데이터는 다른 곳에 전달한 후에 값이 변경되면 그 변경이 반영됩니다. 따라서 Phaser 초기화 시 인자로 객체를 넘겨주는 방식으로 진행하면, 이후에 그 객체의 값을 업데이트하면 반영이 됩니다.

아 그냥 인자로 줘도 되는거였는듯

아니 인자로 주니까 업데이트가 안되는데?

네, 말씀하신대로 JavaScript에서 인자를 넘길 때 그 인자가 객체나 배열 같은 참조형 데이터라면 참조가 전달되지만, React의 상태(useState)는 비동기적으로 동작하므로, 넘길 때 값이 즉시 업데이트되지 않을 수 있습니다. 이는 React에서 상태가 바로 업데이트되지 않는 특성 때문입니다.

즉, 인자로 객체를 넘겨주는 방식은 React에서 상태의 비동기 업데이트 때문에 Phaser와 잘 맞지 않을 수 있습니다.

야이 씨...

ref를 인자로 넣어도 안됨. ref는 이미 사라지고 없기 때문임. 차라리 브라우저에 값 저장해놓는 방식 쓰고 싶은데 그건 어떰?

브라우저에 값을 저장하는 방식은 좋은 대안이 될 수 있습니다. 브라우저의 로컬 저장소(Local Storage) 또는 세션 저장소(Session Storage)를 사용하면, 리액트 컴포넌트가 언마운트되거나 페이지가 새로고침돼도 데이터를 유지할 수 있습니다.

Local Storage:
데이터를 브라우저에 영구적으로 저장합니다. 사용자가 페이지를 새로고침하거나 브라우저를 닫아도 데이터가 유지됩니다.

Session Storage:
데이터를 세션 동안만 저장합니다. 즉, 브라우저 탭을 닫으면 데이터가 사라집니다.

이거면 될듯.

시작할 때 세션 스토리지 지워서 초기화하고 싶은데 가능?

네, 시작할 때 Session Storage를 초기화하는 것은 가능합니다. 컴포넌트가 처음 렌더링될 때 또는 앱이 시작될 때 sessionStorage.clear()를 호출하면 저장된 모든 세션 데이터를 삭제할 수 있습니다.

  // 컴포넌트가 처음 렌더링될 때 세션 스토리지 초기화
  useEffect(() => {
    sessionStorage.clear(); // 세션 스토리지 초기화
  }, []); // 빈 배열을 넣어 컴포넌트가 처음 마운트될 때만 실행

구현 완료.

글자 생성

켜놓고 몇 초 지나면 노트북 팬이 웽 하고 돌아가기 시작함.
오래 켜놓으면 생성이 점점 빨라지기도 함. 뭔가 문제가 있는게 분명.

  useEffect(() => {
    if (isCanvasVisible)
      setInterval(createLetter, 100); // 0.1초마다 새로운 글자를 생성
  }, [isCanvasVisible]); // 캔버스가 표시될 때만 실행

일단 이렇게 하면 여러 번 인터벌 되는 건 막아질거라고 해서 적용하긴 했음.
이후 모니터링 해보니까 생성 빨라지는 현상은 해결

스위치로 글자 생성 토글

useState는 app.js 말고 다른 데에서는 못 쓰는건가?

useState는 React의 훅(hook) 중 하나로, React 컴포넌트 내에서만 사용할 수 있습니다. React 훅은 컴포넌트의 상태를 관리하는 데 사용되기 때문에, React 함수형 컴포넌트 내에서만 호출할 수 있으며, 그렇지 않은 파일이나 함수에서는 사용할 수 없습니다.

따라서 useState를 React 컴포넌트가 아닌 다른 곳에서 사용하려면 몇 가지 제한이 있습니다. 예를 들어, Phaser.js와 같은 파일에서 useState를 직접 사용할 수는 없지만, 몇 가지 방법으로 React 컴포넌트와 연동할 수 있습니다.

다른 js 파일에서 function app 선언하면 걔도 컴포넌트 되는거임?

리액트에서 컴포넌트는 일반적으로 React 함수형 컴포넌트 또는 클래스형 컴포넌트로 정의됩니다. 즉, 단순히 자바스크립트 파일에 함수를 선언한다고 해서 그 함수가 리액트 컴포넌트가 되는 것은 아닙니다. 리액트 컴포넌트가 되기 위해서는 그 함수가 React의 규칙을 따라야 합니다.

1. 리액트 컴포넌트란?

리액트 컴포넌트는 기본적으로 다음의 두 가지 특징을 충족해야 합니다:

  • React JSX 반환: 리액트 컴포넌트는 JSX를 반환하여 화면에 UI를 렌더링합니다.
  • React 훅 사용 가능: 함수형 컴포넌트에서는 useState, useEffect 등의 리액트 훅을 사용할 수 있습니다.

예시: 리액트 컴포넌트 선언

함수형 컴포넌트

import React from 'react';

function App() {
  return (
    <div>
      <h1>Hello, World!</h1>
    </div>
  );
}

export default App;

이 코드는 리액트 컴포넌트입니다. 이 컴포넌트는 JSX(<div>...</div>)를 반환하며, 리액트 생명주기에서 관리됩니다.

자바스크립트의 일반 함수

function someFunction() {
  return "Hello, World!";
}

위 코드는 자바스크립트의 일반적인 함수로, 이 함수는 리액트 컴포넌트가 아닙니다. 단순히 문자열을 반환하며 리액트의 JSX 또는 리액트 훅과는 아무런 연관이 없습니다.

2. 다른 자바스크립트 파일에서 컴포넌트 선언하기

리액트에서는 여러 자바스크립트 파일에 컴포넌트를 선언하고 사용할 수 있습니다. 각 파일에 함수를 선언할 때 그 함수가 JSX를 반환하고 React 훅을 사용할 수 있는 구조로 만들어야만 컴포넌트로 인식됩니다.

예시: 다른 파일에서 컴포넌트 선언

SomeComponent.js
import React from 'react';

function SomeComponent() {
  return (
    <div>
      <p>This is SomeComponent</p>
    </div>
  );
}

export default SomeComponent;
App.js
import React from 'react';
import SomeComponent from './SomeComponent';

function App() {
  return (
    <div>
      <h1>Main App Component</h1>
      <SomeComponent />
    </div>
  );
}

export default App;

여기서 SomeComponent.js에 선언된 함수는 JSX를 반환하므로 리액트 컴포넌트로 인식됩니다. App.js에서 SomeComponent를 불러와 사용할 수 있습니다.

3. 일반 함수와 리액트 컴포넌트의 차이

일반 함수는 다음과 같은 특징을 가집니다:

  • JSX를 반환하지 않습니다.
  • 리액트 훅(useState, useEffect 등)을 사용할 수 없습니다.
  • 리액트의 컴포넌트 생명주기와 관련이 없습니다.

반면, 리액트 컴포넌트는:

  • JSX를 반환하고, 이를 통해 화면에 UI를 렌더링합니다.
  • 리액트 훅을 사용할 수 있습니다.
  • 리액트가 컴포넌트를 관리하며, 상태(state)와 생명주기(lifecycle)를 처리합니다.

리액트 컴포넌트를 쓰는 건 조금 복잡해질수도
원래대로라면 제일 간단한 길이었겠지만
페이저까지 같이 쓰다보니 뭔가 꼬여버릴 것 같음.

일반 자바스크립트 파일에서

import { fireBullet } from "./component/bulletUtils";

이런거 하면 쓸 수는 있는거지?

일반 자바스크립트 파일에서도 ES6 모듈을 사용하여 import와 export를 통해 다른 파일에서 함수를 불러와 사용할 수 있습니다.

아 이러면 그냥 페이저에서 let으로 intervalid 관리하면 될듯?

끄아악 적들의 습격이다

토글이 안됨

해당 함수에서 변수 값을 바꾸니까 intervalid가 갱신이 안돼서 그랬음.
return으로 반환하고 phaserGame.js에서 갱신시키니까 해결.

      if (playerTouchingSwitchButton) {
        intervalId = switchButtonPressed(switchButton, intervalId); // 인터벌 ID 저장
      }

PhaserGame.js

export const switchButtonPressed = (switchButton, intervalId) => {
    console.log("switchButton");

    if (intervalId) {
        clearInterval(intervalId); // 인터벌 제거
    } else {
        const newIntervalId = setInterval(createLetter, 100); // 0.1초마다 새로운 글자를 생성
        return newIntervalId; // 인터벌 ID 반환
    }
}

buttonUtils.js

문자 토글 구현 완료.

일단 여기까지

대충 해야할 거 구현은 다 끝낸 것 같고,
나머지는 폴리싱인데

사소한 것들은 웬만하면 다 다듬었고
남은 큰 것들은

스위치 토글하면 대포도 올라오는 연출,
페이저 시작되면 우주선 불꽃 나오는 연출
버튼 누르는 애니메이션, 스위치 누르는 애니메이션, 문 들어갔다가 나오는 애니메이션
AWS 올리기

정도가 있을듯

근데 생각해보니 귀찮. 할만큼 한듯?

애니메이션 추가

키 누르면 눌렸다가 다시 올라오는 코드 추가.

      if (playerTouchingDeleteButton) {
        deleteButton.setTexture('delete_pressed'); // 눌림 이미지로 변경
        setTimeout(() => deleteButton.setTexture('delete'), 200); // 200ms 후 원래 이미지로 복구
        const textarea = inputField.node.querySelector('textarea');
        textarea.value = textarea.value.slice(0, -1); // 마지막 글자만 제거
        console.log("deleteButton");
      }
      if (playerTouchingSubmitButton) {
        submitButton.setTexture('submit_pressed'); // 눌림 이미지로 변경
        setTimeout(() => submitButton.setTexture('submit'), 200); // 200ms 후 원래 이미지로 복구
        submitButtonPressed(submitButton);
      }

원래 페이저 애니메이션이 이런 식이야?

아니요, Phaser의 애니메이션은 보통 제가 위에서 사용한 방식과는 다르게, Phaser 애니메이션 시스템을 사용합니다. Phaser에는 이미지 텍스처를 단일 프레임으로 전환하는 대신, 프레임 기반 애니메이션을 만드는 시스템이 있습니다. 이 시스템은 스프라이트 시트개별 프레임을 빠르게 전환하는 방식으로 애니메이션을 처리합니다.

Phaser 애니메이션 시스템의 주요 특징:

  • 스프라이트 시트: 여러 프레임을 한 이미지에 모아 놓고, 각각의 프레임을 순차적으로 재생하여 애니메이션을 만듭니다.
  • 프레임 기반 애니메이션: 여러 개의 이미지 프레임을 등록한 후, 순서대로 재생하거나 반복 재생할 수 있습니다.

Phaser 애니메이션 예시

만약 스프라이트 시트를 사용하여 애니메이션을 만들고 싶다면, Phaser의 create 메서드에서 애니메이션을 등록한 다음 재생할 수 있습니다.

1. 스프라이트 시트 프리로드

애니메이션에 필요한 스프라이트 시트를 미리 로드합니다.

function preload() {
  // 스프라이트 시트 로드 (각 프레임의 크기가 32x32라고 가정)
  this.load.spritesheet('button_sprites', '/assets/button_spritesheet.png', {
    frameWidth: 32,
    frameHeight: 32
  });
}

2. 애니메이션 생성 및 재생

create 메서드에서 애니메이션을 정의하고, 특정 상황에서 재생할 수 있습니다.

function create() {
  // 버튼을 추가
  const button = this.add.sprite(400, 300, 'button_sprites', 0).setInteractive();

  // 애니메이션 생성 (스프라이트 시트의 프레임들을 순서대로 재생)
  this.anims.create({
    key: 'button_press',
    frames: this.anims.generateFrameNumbers('button_sprites', { start: 0, end: 1 }), // 0번 프레임에서 1번 프레임까지
    frameRate: 10,  // 초당 프레임 수
    repeat: 0       // 1회 실행 후 멈춤
  });

  // 버튼을 클릭했을 때 애니메이션 재생
  button.on('pointerdown', () => {
    button.play('button_press');
  });
}

Phaser 애니메이션 시스템을 사용하는 장점:

  1. 프레임 기반 애니메이션:

    • 여러 프레임을 순차적으로 재생하여 부드러운 애니메이션을 만듭니다.
    • 예를 들어, 버튼이 눌리는 동안 여러 프레임을 빠르게 전환하여 자연스러운 애니메이션을 표현할 수 있습니다.
  2. 애니메이션 관리가 간편:

    • Phaser 애니메이션 시스템은 애니메이션의 시작, 종료, 반복 등의 상태를 간편하게 관리할 수 있습니다.
    • this.anims.play 메서드를 사용하여 애니메이션을 손쉽게 재생하고, 재생이 끝나면 자동으로 중단됩니다.

결론:

  • Phaser 애니메이션 시스템은 프레임 기반으로 동작하며, 스프라이트 시트를 사용하여 여러 상태를 표현할 때 유용합니다.
  • 이미지 텍스처를 단순히 교체하는 방식도 간단하게 쓸 수 있지만, 여러 프레임의 움직임이 필요할 때는 스프라이트 시트를 사용하는 것이 좋습니다.
  • 버튼과 같은 상호작용 애니메이션에서는 프레임 기반 애니메이션을 사용하면 더 부드럽고 자연스러운 효과를 낼 수 있습니다.

에이 애니메이션 써보려면 스프라이트가 필요한거네
너무 시간이 많이 들어갈 것 같아서 진짜 여기까지

📌 최종 결과물

https://github.com/lazyArtisan/website1



⚔️ 백준


📌 1254 팰린드롬 만들기

# 뒤에서 부터 읽은 문자열의 첫번째 문자가 앞읽 문자열에서 발견됐다면
# 그 다음 문자가 뒤읽 문자열의 다음 문자와 같은지 보고, 같지 않다면 탈락
# 앞읽 문자열의 마지막 문자까지 확인했다면 통과. 
# 문자열의 길이에서 통과한 문자열 빼면 답.
import sys
input = sys.stdin.readline
S = input().strip()
iS = ""
for i in range(len(S)-1,-1,-1): # 세번째 인자 빼먹어서 헤맴
    iS+=S[i]
while 1:
    i=0
    while i < len(S) and S[i]==iS[i]:
        i+=1
    if i==len(S):
        break
    else:
        S=S[i+1:]
print(2*len(iS)-len(S))

아무리 생각해도 로직이 틀린게 없는 것 같길래
기분이 더럽지만 질문 게시판을 봄.

예제: aaabbbaa
내 출력: 14
답: 9

# 뒤에서 부터 읽은 문자열의 첫번째 문자가 앞읽 문자열에서 발견됐다면
# 그 다음 문자가 뒤읽 문자열의 다음 문자와 같은지 보고, 같지 않다면 탈락
# 앞읽 문자열의 마지막 문자까지 확인했다면 통과. 
# 문자열의 길이에서 통과한 문자열 빼면 답.
import sys
input = sys.stdin.readline
S = input().strip()
iS = ""
for i in range(len(S)-1,-1,-1): # 세번째 인자 빼먹어서 헤맴
    iS+=S[i]
while 1:
    i=0
    while i < len(S) and S[i]==iS[i]:
        i+=1
    if i==len(S):
        break
    else:
        S=S[1:]
print(2*len(iS)-len(S))

같은 거였다고 건너뛰어버리면 생략되는 경우의 수가 생기는 거였음.
인덱스 하나하나씩

실전에서 이런거 당할수도 있을 거 생각하니까 기분이 너무 더럽다.

내가 실행하는 연산이 정확히 무엇을 하는지,
그러니까 어떻게 무엇을 건너뛰고 건너뛰지 않는지,
그 건너뜀에는 확실한 근거가 있는지 생각을 하고 코드를 적어야 됨...

내가 왜 저렇게 코드를 짰는지 복기해보니,
그냥 i+1부터 문자열을 자른다는 거에 아무런 자각이 없었음.
졸려서 그런 건 알겠는데, 졸렸으면 저 코드를 짜지를 못했어야 함.
내가 이 코드를 왜 짜야 하는지에 대한 근거가 없으면 짜지를 마셈.

📌 H-index (LeetCode)

어... 풀긴 했는데 이건 좀

# 배열 5000개 세워놓고 기록한 다음에 역순으로 더해나가다가
        # 내가 지금 보고 있는 인덱스보다 cnt가 같거나 높다면 그게 H-index
        cntL=[0]*5000
        for c in citations:
            cntL[c]+=1
        cnt=0
        for i in range(4999,-1,-1):
            cnt+=cntL[i]
            if cnt >= i:
                break
        return i

발전하긴 했는데 여전히 쓰레기.

일단 내일 마저 하자.

0개의 댓글