반응형

docker-compose ps 실행시 발생할 수 있는 오류

Traceback (most recent call last):
  File "urllib3/connectionpool.py", line 670, in urlopen
  File "urllib3/connectionpool.py", line 392, in _make_request
  File "http/client.py", line 1255, in request
  File "http/client.py", line 1301, in _send_request
  File "http/client.py", line 1250, in endheaders
  File "http/client.py", line 1010, in _send_output
  File "http/client.py", line 950, in send
  File "docker/transport/unixconn.py", line 43, in connect
ConnectionRefusedError: [Errno 61] Connection refused

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "requests/adapters.py", line 439, in send
  File "urllib3/connectionpool.py", line 726, in urlopen
  File "urllib3/util/retry.py", line 410, in increment
  File "urllib3/packages/six.py", line 734, in reraise
  File "urllib3/connectionpool.py", line 670, in urlopen
  File "urllib3/connectionpool.py", line 392, in _make_request
  File "http/client.py", line 1255, in request
  File "http/client.py", line 1301, in _send_request
  File "http/client.py", line 1250, in endheaders
  File "http/client.py", line 1010, in _send_output
  File "http/client.py", line 950, in send
  File "docker/transport/unixconn.py", line 43, in connect
urllib3.exceptions.ProtocolError: ('Connection aborted.', ConnectionRefusedError(61, 'Connection refused'))

 

 

연결 거부라는 메시지가 포함이 되어 있어 혹시나 싶어 docker ps 명령을 실행하니 

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

 

위와 같은 메시지가 출력이 되었네요.

 

이전에 docker desktop을 종료시켜 두었는데 다시 실행시키고 docker ps, docker-compose ps 명령을 실행하니 이상없이 실행이 됐습니다.

 

관련 오류를 검색해보면 환경변수같은 다른 이유도 있는 듯 한데 일단 docker가 실행되고 있는지 확인 필수 인거 같습니다.

728x90
반응형
반응형

Step1: 프로젝트 초기 설정

패키지 관리자는 yarn을 사용였습니다. 

먼저 프로젝트에 대한 새 디렉토리를 만들고 해당 디렉토리로 이동 후  Yarn 프로젝트를 초기화합니다.

mkdir my-project
cd my-project
yarn init -y

 

Step2: 프로젝트 초기 설정

Node.js, TypeScript, Express, TypeORM 및 PostgreSQL에 대한 패키지를 설치합니다.

yarn add express typeorm reflect-metadata pg
yarn add -D typescript ts-node @types/node @types/express

 

1. 'yarn add express typeorm reflect-metadata pg'

이 명령어는 express, typeorm, reflect-metadata, pg 패키지를 설치합니다. 이 패키지들은 프로젝트의 런타임 의존성으로 설치됩니다.

  • express: Node.js를 위한 빠르고 간단한 웹 프레임워크입니다. HTTP 서버를 쉽게 구축할 수 있게 해줍니다.
  • typeorm: TypeScript와 JavaScript(ES7, ES6, ES5)를 위한 ORM(Object Relational Mapper)입니다. 데이터베이스와 상호작용을 쉽게 해줍니다.
  • reflect-metadata: 메타데이터를 사용하여 객체지향 프로그래밍을 지원하는 데 필요한 라이브러리입니다. TypeORM에서 데코레이터 기능을 사용할 때 필요합니다.
  • pg: PostgreSQL 데이터베이스와 상호작용하기 위한 PostgreSQL 클라이언트입니다.

2. 'yarn add -D typescript ts-node @types/node @types/express'

이 명령어는 typescript, ts-node, @types/node, @types/express 패키지를 개발 의존성으로 설치합니다. 즉, 이 패키지들은 개발 중에만 필요하며, 프로덕션 빌드에는 포함되지 않습니다.

  • typescript: TypeScript 컴파일러입니다. TypeScript 코드를 JavaScript로 변환해줍니다.
  • ts-node: TypeScript 실행 환경입니다. TypeScript 코드를 실행할 수 있게 해줍니다.
  • @types/node: Node.js의 타입 정의 파일입니다. TypeScript에서 Node.js 내장 모듈을 사용할 때 타입 정보를 제공합니다.
  • @types/express: Express의 타입 정의 파일입니다. TypeScript에서 Express를 사용할 때 타입 정보를 제공합니다.

Step3: TypeScript 설정

프로젝트 루트경로에 tsconfig.json 파일을 생성하고 TypeScript 설정을 구성합니다.

{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}

 

Step4: TypeORM 구성 설정

프로젝트 루트경로에 src/data-source.ts 파일을 생성하고 설정을 구성합니다.

import "reflect-metadata";
import { DataSource } from "typeorm";
import { User } from "./entity/User";

export const AppDataSource = new DataSource({
  type: "postgres",
  host: "localhost",
  port: 5432,
  username: "your-username",
  password: "your-password",
  database: "your-database",
  synchronize: true,
  logging: false,
  entities: [User],
  migrations: [],
  subscribers: [],
});

 

'your-username', 'your-password', 'your-database'는 PostgreSQL 설치할때 설정 값으로 바꿉니다

 

Step:5 엔티티 생성

User 및 GenderStatistics 엔티티를 생성합니다.

User 엔티티

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  firstName!: string;

  @Column()
  lastName!: string;

  @Column()
  age!: number;
}

 

GenderStatistics 엔티티

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

@Entity()
export class GenderStatistics {
  @PrimaryGeneratedColumn()
  num!: number;

  @Column({ nullable: true })
  poll_id!: string;

  @Column({ nullable: true })
  trait!: string;

  @Column({ nullable: true })
  gender!: string;

  @Column({ nullable: true })
  gender_cnt!: number;

  @Column("float", { nullable: true })
  rate!: number;
}

 

rate 필드는 소수점 값으로 저장할 수 있도록 설정하였습니다.

 

Step6: 리포지토리 생성

위의서 만든 각 엔티티에 해당하는 리포지토리를 생성하여 데이터베이스 작업을 처리합니다.

 

userRepository.ts

import { AppDataSource } from "../data-source";
import { User } from "../entity/User";

const userRepository = AppDataSource.getRepository(User);

export const createUser = async (userData: Partial<User>): Promise<User> => {
  const user = userRepository.create(userData);
  return await userRepository.save(user);
};

export const getUserById = async (id: number): Promise<User | null> => {
  return await userRepository.findOneBy({ id });
};

 

genderStatisticsRepository.ts

import { AppDataSource } from "../data-source";
import { GenderStatistics } from "../entity/GenderStatistics";

const genderStatisticsRepository = AppDataSource.getRepository(GenderStatistics);

export const createGenderStatistics = async (data: Partial<GenderStatistics>): Promise<GenderStatistics> => {
  const genderStat = genderStatisticsRepository.create(data);
  return await genderStatisticsRepository.save(genderStat);
};

export const getGenderStatisticsByNum = async (num: number): Promise<GenderStatistics | null> => {
  return await genderStatisticsRepository.findOneBy({ num });
};

 

Step7: 컨트롤러 생성

컨트롤러를 생성하여 HTTP 요청을 처리합니다.

 

userController.ts

import { Request, Response } from "express";
import { createUser, getUserById } from "../repositories/userRepository";

export const createUserHandler = async (req: Request, res: Response) => {
  try {
    const user = await createUser(req.body);
    res.status(201).json(user);
  } catch (error) {
    if (error instanceof Error) {
      res.status(500).json({ message: error.message });
    } else {
      res.status(500).json({ message: "An unknown error occurred" });
    }
  }
};

export const getUserByIdHandler = async (req: Request, res: Response) => {
  try {
    const user = await getUserById(parseInt(req.params.id));
    if (user) {
      res.json(user);
    } else {
      res.status(404).json({ message: "User not found" });
    }
  } catch (error) {
    if (error instanceof Error) {
      res.status(500).json({ message: error.message });
    } else {
      res.status(500).json({ message: "An unknown error occurred" });
    }
  }
};

 

genderStatisticsController.ts

import { Request, Response } from "express";
import { createGenderStatistics, getGenderStatisticsByNum } from "../repositories/genderStatisticsRepository";

export const createGenderStatisticsHandler = async (req: Request, res: Response) => {
  try {
    const genderStat = await createGenderStatistics(req.body);
    res.status(201).json(genderStat);
  } catch (error) {
    if (error instanceof Error) {
      res.status(500).json({ message: error.message });
    } else {
      res.status(500).json({ message: "An unknown error occurred" });
    }
  }
};

export const getGenderStatisticsByNumHandler = async (req: Request, res: Response) => {
  try {
    const genderStat = await getGenderStatisticsByNum(parseInt(req.params.num));
    if (genderStat) {
      res.json(genderStat);
    } else {
      res.status(404).json({ message: "Gender statistics not found" });
    }
  } catch (error) {
    if (error instanceof Error) {
      res.status(500).json({ message: error.message });
    } else {
      res.status(500).json({ message: "An unknown error occurred" });
    }
  }
};

 

Step8: 라우트 설정

userRoutes.ts

import { Router } from "express";
import { createUserHandler, getUserByIdHandler } from "../controllers/userController";

const router = Router();

router.post("/users", createUserHandler);
router.get("/users/:id", getUserByIdHandler);

export default router;

 

genderStatisticsRoutes.ts

import { Router } from "express";
import { createGenderStatisticsHandler, getGenderStatisticsByNumHandler } from "../controllers/genderStatisticsController";

const router = Router();

router.post("/gender-statistics", createGenderStatisticsHandler);
router.get("/gender-statistics/:num", getGenderStatisticsByNumHandler);

export default router;

 

Step9: Express 서버 설정

index.ts

import "reflect-metadata";
import express from "express";
import { AppDataSource } from "./data-source";
import userRoutes from "./routes/userRoutes";
import genderStatisticsRoutes from "./routes/genderStatisticsRoutes";

AppDataSource.initialize().then(() => {
  const app = express();

  app.use(express.json());

  app.use(userRoutes);
  app.use(genderStatisticsRoutes);

  const port = process.env.PORT || 3000;
  app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
  });
}).catch(error => console.log(error));

 

모두 작성 되었다면 yarn dev로 개발 서버를 실행합니다.

 

자바스크립트 계열은 자료를 찾다보면 작성자마다 구조와 형식이 달라 늘 어렵네요.

728x90
반응형
반응형

JavaScript의 옵셔널 체이닝(Optional Chaining) 연산자 ?.는 객체의 속성에 접근할 때, 해당 속성이 존재하지 않더라도 에러를 발생시키지 않고 undefined를 반환하는 유용한 문법입니다. 이는 깊게 중첩된 객체 구조에서 속성에 접근할 때 특히 유용하며, 코드의 가독성과 안전성을 크게 향상시켜줍니다.

왜 옵셔널 체이닝이 필요한가?

객체의 속성에 접근할 때, 종종 해당 속성이 존재하지 않을 가능성을 고려해야 합니다. 예를 들어, 서버로부터 받은 데이터가 항상 동일한 구조를 가지지 않거나, 어떤 필드가 조건에 따라 존재하지 않을 수 있습니다. 이러한 상황에서 안전하게 속성에 접근하기 위해서는 많은 방어적 코드를 작성해야 합니다.

let person = {}; // person 객체는 비어있습니다.

console.log(person.contactInfo.address); // TypeError: Cannot read property 'address' of undefined

 

위의 코드에서 user.address가 undefined이기 때문에 user.address.street에 접근하려고 하면 오류가 발생합니다. 이 문제를 해결하기 위해 보통 아래와 같은 방어적 코드를 작성합니다.

let person = {};

if (person && person.contactInfo && person.contactInfo.address) {
  console.log(person.contactInfo.address);
} else {
  console.log('주소가 없습니다.');
}

 

하지만 이러한 방어적 코드는 가독성을 떨어뜨리고 작성하기 번거로울 수 있습니다. 이를 해결하기 위해 등장한 것이 옵셔널 체이닝 연산자입니다.

옵셔널 체이닝의 사용법

옵셔널 체이닝 연산자를 사용하면 중첩된 객체의 속성에 안전하게 접근할 수 있습니다. 옵셔널 체이닝은 ?. 형태로 사용되며, 해당 속성이 존재하지 않을 경우 undefined를 반환합니다.

let person = {}; // person 객체는 여전히 비어있습니다.

console.log(person?.contactInfo?.address); // undefined

 

위의 코드에서 user.address가 undefined이더라도 오류가 발생하지 않고 undefined를 반환합니다.

옵셔널 체이닝의 다양한 사용 예

1. 객체 속성 접근

let person = {
  fullName: 'John Doe',
  contactInfo: {
    address: '456 Elm St'
  }
};

console.log(person?.contactInfo?.address); // '456 Elm St'
console.log(person?.employment?.position); // undefined

 

2. 함수 호출

옵셔널 체이닝은 함수 호출에도 사용할 수 있습니다. 함수가 존재하지 않을 경우 undefined를 반환합니다.

let person = {
  sayHello: function() {
    console.log('Hi there!');
  }
};

person.sayHello?.(); // 'Hi there!'
person.sayGoodbye?.(); // 아무 일도 일어나지 않음

 

3. 배열 요소 접근

옵셔널 체이닝은 배열의 요소 접근에도 사용할 수 있습니다.

let employees = [{ fullName: 'John Doe' }, { fullName: 'Jane Smith' }];

console.log(employees[0]?.fullName); // 'John Doe'
console.log(employees[3]?.fullName); // undefined

 

결론

옵셔널 체이닝 연산자는 객체의 속성에 안전하게 접근할 수 있게 해주는 강력한 도구입니다. 이를 통해 코드의 가독성을 높이고, 중첩된 객체 구조를 다룰 때 발생할 수 있는 오류를 예방할 수 있습니다. JavaScript 코드에서 옵셔널 체이닝을 적절히 활용하면 보다 안정적이고 유지보수하기 쉬운 코드를 작성할 수 있습니다.

728x90
반응형
반응형

PostgreSQL에서 real, float, float8, double precision, numeric 타입은 모두 부동 소수점 숫자 또는 고정 소수점 숫자를 저장하는 데 사용됩니다. 각각의 차이점과 사용 방법, 그리고 활용 방안에 대해 알아보겠습니다.

데이터 타입 설명

  1. real
    • 설명: 4바이트 단정밀도 부동 소수점 숫자.
    • 정밀도: 약 6자리의 소수점 자릿수를 가짐.
    • 범위: -3.4E+38 ~ +3.4E+38.
    • 사용: 메모리가 제한적이고, 매우 높은 정밀도가 필요하지 않은 경우에 사용.
  2. float (또는 float8)
    • 설명: 8바이트 배정밀도 부동 소수점 숫자. float8은 double precision의 별칭입니다.
    • 정밀도: 약 15자리의 소수점 자릿수를 가짐.
    • 범위: -1.7E+308 ~ +1.7E+308.
    • 사용: 높은 정밀도가 필요한 경우에 사용.
  3. double precision
    • 설명: 8바이트 배정밀도 부동 소수점 숫자.
    • 정밀도: float와 동일, 약 15자리의 소수점 자릿수를 가짐.
    • 범위: float와 동일, -1.7E+308 ~ +1.7E+308.
    • 사용: 매우 높은 정밀도가 필요한 경우에 사용.
  4. numeric (또는 decimal)
    • 설명: 가변 길이의 정확한 숫자. 소수점 이하 자릿수와 전체 자릿수를 지정할 수 있음.
    • 정밀도: 사용자가 지정한 자릿수까지 정확함.
    • 범위: 매우 넓음, 제한이 없다고 볼 수 있음.
    • 사용: 금액 계산 등 매우 높은 정밀도가 요구되는 경우에 사용.

사용 방법과 예제

real

CREATE TABLE example_real ( id serial PRIMARY KEY, value real ); 
INSERT INTO example_real (value) VALUES (3.14), (2.71); 
SELECT * FROM example_real;

float (float8), double precision

CREATE TABLE example_float ( id serial PRIMARY KEY, value float8 ); 
CREATE TABLE example_double_precision ( id serial PRIMARY KEY, value double precision ); 
INSERT INTO example_float (value) VALUES (3.141592653589793), (2.718281828459045); 
INSERT INTO example_double_precision (value) VALUES (3.141592653589793), (2.718281828459045); 
SELECT * FROM example_float; SELECT * FROM example_double_precision;

numeric (decimal)

CREATE TABLE example_numeric ( id serial PRIMARY KEY, value numeric(10, 2) ); 
INSERT INTO example_numeric (value) VALUES (12345.67), (98765.43); 
SELECT * FROM example_numeric;
 

활용 방안

  1. real: 센서 데이터, 간단한 계산 등 정밀도가 크게 필요 없는 경우에 사용합니다.
  2. float (float8), double precision: 과학 계산, 통계 분석 등 높은 정밀도가 필요한 경우에 사용합니다. float8과 double precision은 동일한 타입이므로 동일한 용도로 사용됩니다.
  3. numeric (decimal): 금액, 금융 데이터 등 매우 높은 정밀도와 정확성이 필요한 경우에 사용합니다. 특히, 소수점 이하 자릿수가 중요한 경우 유용합니다.

요약

  • real은 4바이트 단정밀도 부동 소수점으로, 정밀도가 크게 필요 없는 경우에 사용됩니다.
  • float와 double precision은 8바이트 배정밀도 부동 소수점으로, 높은 정밀도가 필요한 경우에 사용됩니다.
  • numeric은 고정 소수점으로, 정밀도가 매우 중요한 경우에 사용됩니다.
728x90
반응형
반응형

어느날 부터인가 intellij에서 종종 뜨는 에러!!

git install하라는 해결책을 제공해주지만 설치해도 문제가 해결되지 않고 동일한 메시지가 계속 노출이 됩니다.

설치해도 계속 나오는 에러 메시지!!! 원인은 intellij에 설정되어 있는 git 설치 주소를 제대로 인식하지 못하여 방생하는 문제라고 하네요. 

해당 문제가 발생하게 되면 수정된 파일이  project file tree에 표시가 제대로 되지 않고 git diff도 제대로 동작하지 않게 됩니다. 

 

해결 방법

  1. 터미널에서 which git 명령어로 위치를 확인
  2. Preference -> Version Control -> Git -> Path to Git executable에 등록되어 있는 주소를 변경해줌
  3.  Path to Git executable 입력 창 옆에 Test를 실행해보면 설정한 경로에 있는 깃의 버전을 알려줌

해결하면서 확인한것 중  터미널에서 확인한 git의 경로와  이전 설정경로의 git 둘다 버전이 출력된다는 것입니다. 버전이 다르긴 하지만 git을 실행하는데는 문제가 없다는것인데 intellij가 제대로 인식하지 못하는 이유는 의문이네요.

 

 

728x90
반응형
반응형

 

1. Firebase App Check

- 앱의 보안성을 강조하기 위한 서비스 입니다. 앱의 무단 접근, 자동화 봇 등에 의한 보안 위협으로부터 앱을 보호하면, 신뢰할 수 있는 사용자만이 앱에 액세스 할 수 있도록 보장합니다.

- Firebase App Check는 클라이언트-서버 인증 방식을 사용하여 동작합니다. 앱 클라이언트는 인증된 앱으로 등록되어 있는지 확인하기 위해 Firebase App Check SDK를 사용하여 앱 서버에 요청을 보내고, 서버는 이 요청에 대해 Firebase App Check API를 사용하여 요청을 검증합니다. 이를 통해 클라이어트가 신뢰할 수 있는 Firebase 프로젝트에 속하는 앱인지 확인하고, 무단 액세스를 차단할 수 있습니다.

 

2. Flutter에 적용하기

  • Firebase Console에서 Firebase 프로젝트를 생성하고, 해당 프로젝트에 Firebase App Check를 활성화합니다.
  • Flutter 프로젝트의 pubspec.yaml 파일에  Firebase App Check Flutter 패키지를 추가합니다. 
dependencies:
  firebase_app_check: ^0.7.0
  • Flutter 프로젝트의 main.dart 파일에 Firebase App Check를 구성합니다. 
import 'package:firebase_app_check/firebase_app_check.dart';
import 'package:flutter/material.dart';

void main() async {
  // Firebase 초기화
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  // Firebase App Check 활성화
  FirebaseAppCheck.instance.activate(webRecaptchaSiteKey: 'YOUR_RECAPTCHA_SITE_KEY');

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Firebase App Check Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter Firebase App Check Demo'),
        ),
        body: Center(
          child: Text('Hello World!'),
        ),
      ),
    );
  }
}
  • Firebase App Check를 사용하여 네트워크 요청을 검증하기 위해 package:http 패키지를 사용하여 네트워크 요청을 보낼 때, FirebaseAppCheckToken을 추가합니다.
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:firebase_app_check/firebase_app_check.dart';

final _httpClient = http.Client();

Future<String> fetchData() async {
  // Firebase App Check 토큰 가져오기
  final FirebaseAppCheckToken appCheckToken = await FirebaseAppCheck.instance.getToken();

  // 네트워크 요청 보내기
  final response = await _httpClient.get(
    Uri.parse('https://example.com/data.json'),
    headers: {
      // Firebase App Check 토큰 헤더 추가
      'X-Firebase-AppCheck': 'Bearer ${appCheckToken.token}',
    },
  );

  // 데이터 반환
  return utf8.decode(response.bodyBytes);
}

 

728x90
반응형
반응형

flutter에서 클래스 내부에 선언된 변수와 메소드를 의미하며 클래스 멤버는 다음과 같이 분류됩니다.

  1. 인스턴스 변수 (Instance Variables) : 객체의 인스턴스에 속한 변수로, 객체를 생성할 때 초기화되며, 객체의 속성이나 상태를 나타냅니다.
  2. 정적 변수 (Static Variables) : 클래스에 속한 변수로, 클래스를 로드할 때 초기화되며, 클래스와 관련된 정보를 나타냅니다.
  3. 인스턴스 메소드 (Instance Methods) : 객체의 인스턴스에 속한 메소드로, 객체를 통해 호출됩니다. 객체의 속성이나 상태를 조작하거나, 객체와 관련된 행동을 나타냅니다.
  4. 정적 메소드 (Static Methods) : 클래스에 속한 메소드로, 클래스를 통해 호출됩니다. 클래스와 관련된 행동을 나타냅니다.

다음은 간단한 Flutter  클래스의 예시입니다.

class Person {
  String name;
  int age;

  Person(this.name, this.age);

  void sayHello() {
    print("Hello, my name is $name and I am $age years old.");
  }

  static String hello() {
    return "Hello from Person class!";
  }
}

위의 코드에서 name과 age는 인스턴스 변수

say Hello()는 인스턴스 메소드

hello()는 정적 메소드 입니다.

728x90
반응형
반응형

인스타그램과 같은 UI를 만들기 위해서 ListView.builder를 사용하여 상하 스크롤을 기본적으로 배치하고 각 페이지를 PageView.builder로  좌우 스크롤이 가능하게 구현을 했다.

실제로는 Firebase의 데이터를 불러서 동적으로 구현하지만 예제 소스는 텍스트 값을 리스트에 넣어서 정리했다.

class _HomePageState extends State<HomePage> {
  final ScrollController _scrollController = ScrollController();
  final PageController _pageController = PageController(initialPage: 1);

  List verticalList = ["1", "2"];
  List horizontalList = ["1", "2", "3"];

  @override
  Widget build(BuildContext context) {
    var width = MediaQuery.of(context).size.width;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Photo Manager'),
      ),
      body: SafeArea(
        child: ListView.builder(
          controller: _scrollController,
          scrollDirection: Axis.vertical,
          shrinkWrap: true,
          itemCount: verticalList.length,
          itemBuilder: (context, index) =>
              SizedBox(
                  width: width,
                  height: width * 1.35,
                  child: PageView.builder(
                    controller: _pageController,
                    itemCount: horizontalList.length,
                    itemBuilder: (context, index) =>
                        Text(horizontalList[index])
                ),
              ))
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: const Icon(Icons.add),
      ),
    );
  }
}

해당 소스를 실행해보면 화면 상하 2개의 위젯과 각 위젯 안에 3개씩의 위젯으로 구성된 결과가 보여지게 된다.

대충 이런 느낌....

상하좌우 스크롤을 해보면 문제없이 잘 동작한다. 화면상으로만 콘솔을 보지 않는다면 말이다.

콘솔을 확인해보면 아래와 같은 메시지를 볼수가 있다.

위의 메세지는 좌우 스크롤을 끝까지 한상태에서 한번더 스크롤을 해보면 발생한다.

'package:flutter/src/widgets/overscroll_indicator.dart': Failed assertion: line 243 pos 14: 'notification.metrics.axis == widget.axis': is not true.

overscroll_indicator.dart 에서 발생한다고 하니 스크롤이 범위를 벗어났다고 하는거 같긴한데 지금까지 해본결과 좌우던 상하던 오버스크롤이 되도 문제가 없었는데 느닷없는 오류에 많이 당황했다.

 

해당 메시지로 구글링을 해봐도 문제를 해결하기 위한 답변을 찾지 못하고 NotificationListener라는 힌트만 얻게 되었다.

그리고 NotificationListener에 대해서 찾아보기 시작한 결과

위와 같은 중첩구조에서  scroll event가 발생할 경우 상위의 위젯(위에선 ListView.builder)에서는 단일 위젯과 같기에 문제 없으나 내부에 있는 위젯(위에선 PageView.builder)에 발생한 event를 상위 위젯에서도 잡는다는 것이다.

page_view.dart나 list_view.dart 소스를 열어보면 안에 NotificationLIstener를 return 하는것을 찾아볼수 있다.

 

그럼 상위위젯에서 listening 하지 못하게 하면 해결되는거 아닌가? 란 생각과 함께 찾아본결과

해당 이벤트를 발생시키는 위젯을 NotificationLIstener위젯으로 감싸서 직접 컨트롤 할 수 있다는걸 알게 되었다.

class _HomePageState extends State<HomePage> {
  final ScrollController _scrollController = ScrollController();
  final PageController _pageController = PageController(initialPage: 1);

  List verticalList = ["1", "2"];
  List horizontalList = ["1", "2", "3"];

  @override
  Widget build(BuildContext context) {
    var width = MediaQuery.of(context).size.width;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Photo Manager'),
      ),
      body: SafeArea(
        child: ListView.builder(
          controller: _scrollController,
          scrollDirection: Axis.vertical,
          shrinkWrap: true,
          itemCount: verticalList.length,
          itemBuilder: (context, index) =>
              SizedBox(
                  width: width,
                  height: width * 1.35,
                  child: NotificationListener<ScrollNotification> (
                    onNotification: (notification) {
                      return true;
                    },
                    child: PageView.builder(
                        controller: _pageController,
                        itemCount: horizontalList.length,
                        itemBuilder: (context, index) =>
                            Text(horizontalList[index])
                    ),
                  ))
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: const Icon(Icons.add),
      ),
    );
  }
}

위처럼 변경하고 실행해보면 더이상 에러가 발생하지 않는다.

NotificationListener로 감싸서  onNotification을 무조건 true로 던졌다. true를 쓴 이유는 notification_listener.dart에 있는 설명에 나와있다.

Return true to cancel the notification bubbling. Return false to allow the notification to continue to be dispatched to further ancestors.

번역해보면

"알림 버블링을 취소하려면 true를 반환합니다. 알림이 추가 조상에게 계속 전달되도록 하려면 false를 반환합니다." 라고 한다.

지금은 상위 위젯에 연계해서 이벤트 처리할 일이 없기에 무조건 true로 처리하지만 내부 위젯과 연계해서 상위 위젯에 어떠한 이벤트를 주고자 한다면 

onNotification: (notification) {
.
.
}

onNotification에 인자로 받은  notification은 실제로 scroll_notification.dart 객체이기 때문에 소스를 열어보면 얻을 수 있는 정보들이 존재함을 확인할 수 있다.

NotificationListener와 관련된 부분은 아직 이해가 부족하고 추측성 결론이 많아 후에 조금 더 공부해 보기로 하고 마무리 한다.

728x90
반응형

+ Recent posts